esteban@devtrillo:~/blog/writing$
← cd -
$ cat learning-effect-1-introduction.md

Part 1: Stop Throwing Errors

Jul 10, 2026·2 min read
$ tail -f learning-effect-1-introduction/readers
connecting…

I want to learn Effect the annoying way: by using it, getting confused, and writing down the parts that finally click.

This is the first note.

Effect is a TypeScript library for writing programs where the scary parts are visible. A function can fail. It can need a database. It can do async work. Normal TypeScript hides a lot of that behind Promise, throw, and comments.

Effect puts those facts in the type.

Effect.Effect<Success, Error, Requirements>;

Read that as:

That is the whole first door.

Start with a normal function

Here is the usual TypeScript version:

const parseUserId = (input: string): number => {
const id = Number(input);
if (!Number.isInteger(id)) {
throw new Error("User id must be an integer");
}
return id;
};

The type says this returns a number.

It does not say it can throw.

So the caller sees this:

const id = parseUserId("abc");

And the crash is hiding off-screen.

What is missing from the type of parseUserId(input: string): number?

The Effect shape

With Effect, failure becomes part of the return value.

import { Effect } from "effect";
class InvalidUserId {
readonly _tag = "InvalidUserId";
constructor(readonly input: string) {}
}
const parseUserId = (input: string) =>
Effect.sync(() => Number(input)).pipe(
Effect.flatMap((id) =>
Number.isInteger(id)
? Effect.succeed(id)
: Effect.fail(new InvalidUserId(input)),
),
);

The important part is not the syntax yet. The important part is the type.

Effect.Effect<number, InvalidUserId, never>;

That says:

never means this effect does not need anything from the outside world. No database. No config. No logger. Nothing.

In Effect.Effect<number, InvalidUserId, never>, what does never mean?

Effect values do not run by themselves

This part matters.

An Effect is a description of work. Creating one does not run it.

const program = parseUserId("123");

That line builds a value. It has not parsed anything yet.

To run it at the edge of your app, you use a runner:

const result = await Effect.runPromise(program);

This is different from calling an async function. With a Promise, the work usually starts right away. With Effect, you can build the program first, combine it with other work, handle errors, provide dependencies, and run it once at the edge.

When does an Effect run?

Why bother?

For this tiny example, you probably would not.

A plain function with a union return is enough:

type ParseResult =
| { ok: true; value: number }
| { ok: false; error: "InvalidUserId" };

Effect starts earning its keep when the program has more than one kind of failure and more than one outside dependency.

For example:

That is where try/catch starts to smear across the code. Effect gives you one pipe where success, failure, retry, timeout, and dependencies stay visible.

NOTE

My rule for now: do not reach for Effect to parse one number. Reach for it when the boring error path is already bigger than the happy path.

The part I am taking with me

The first mental model is simple:

Effect.Effect<Success, Error, Requirements>;

If I can read those three slots, I can start reading Effect code.

Next I want to learn the pipe style: succeed, fail, flatMap, and why Effect code often reads inside-out until it clicks