esteban@devtrillo:~/blog/writing$
← cd -
$ cat learning-effect-11-one-small-real-program.md

Part 11: One Small Real Program

Jul 20, 2026·1 min read
$ tail -f learning-effect-11-one-small-real-program/readers
connecting…

Now I want one small program.

Not a production app.

Not an architecture diagram.

Just enough code to see the pieces touch.

The flow

The program will:

  1. read a user id from a request
  2. parse it
  3. fetch the user
  4. turn UserNotFound into a 404 response
  5. run once at the edge

That is a real shape.

The core program

const getUserResponse = (input: string) =>
parseUserId(input).pipe(
Effect.flatMap(fetchUser),
Effect.map((user) => Response.json(user)),
Effect.catchTag("UserNotFound", () =>
Effect.succeed(new Response("User not found", { status: 404 })),
),
);

Read it straight down.

Parse.

Fetch.

Turn success into JSON.

Handle one known error.

Which error is handled here?

The type tells the truth

After handling UserNotFound, the type might still say:

Effect.Effect<Response, InvalidUserId, UserRepo>;

That means:

There is still work to do before running it.

We need to decide what bad input means.

We need to provide UserRepo.

What still must be provided before this program can run?

Run it at the edge

export async function GET(request: Request) {
const id = new URL(request.url).searchParams.get("id") ?? "";
const program = getUserResponse(id).pipe(
Effect.provide(UserRepoLive),
Effect.catchTag("InvalidUserId", () =>
Effect.succeed(new Response("Invalid user id", { status: 400 })),
),
);
return Effect.runPromise(program);
}

The runner appears once.

At the edge.

That feels right.

What I learned from the first pass

Effect is not one idea.

It is a stack of small ideas:

I still have a lot to learn.

But now I can read the first line of the map.

Effect.Effect<Success, Error, Requirements>;

That is enough to keep going.

NOTE

Next I would learn schedules, retries, scopes, and streams. But not yet. The basics should feel boring first.