Lesson 4: Errors You Can See
The big idea
The card says what can go wrong, so the compiler makes you deal with it. Effect also separates “problems we expected” from “the kitchen caught fire,” and gives you tools like retry and timeout that snap onto any card.
The story
Two very different things can go wrong at the smoothie counter:
- Out of mango. Annoying, but normal. It’s going to happen sometimes. A good recipe card lists it on the “Can go wrong” line, and a good cook has a plan: offer strawberry instead.
- The kitchen catches fire. Nobody writes “kitchen fire” on a smoothie recipe. It’s not a smoothie problem, it’s a bug in the building. You don’t “handle” it with a strawberry.
Effect calls the first kind an error (expected, written on the card) and the second kind a defect (unexpected, a bug). The compiler makes you deal with errors. Defects crash loudly, which is what you want, because hiding a fire is worse than seeing it.
Name tags for errors
Each expected error gets a name tag so you can tell them apart:
import { Data } from "effect"
class OutOfMango extends Data.TaggedError("OutOfMango") {}
class BlenderJammed extends Data.TaggedError("BlenderJammed")<{ readonly speed: number }> {}
That’s a whole error type. The string in quotes is the name tag. Now a card can list them:
import { Effect } from "effect"
const getMango = Effect.gen(function* () {
if (Math.random() < 0.3) {
return yield* Effect.fail(new OutOfMango()) // the "can go wrong" line
}
return "🥭"
})
// getMango : Effect<string, OutOfMango>
// ^ TypeScript figured this out on its own
Nobody wrote OutOfMango in the type. TypeScript read the recipe and filled in the “Can go wrong” line by itself.
The picture: a card going through the kitchen
flowchart TD
A[getMango card] --> B{run it}
B -- "success" --> C[🥭]
B -- "error: OutOfMango" --> D{Did the cook write a plan for OutOfMango?}
D -- "yes: catchTag" --> E[🍓 strawberry instead]
D -- "no" --> F[The card's error line still says OutOfMango.<br/>Whoever runs it must deal with it.]
B -- "defect: kitchen fire" --> G[💥 crash loudly, this is a bug]
Catching one specific error
const getFruit = getMango.pipe(
Effect.catchTag("OutOfMango", () => Effect.succeed("🍓"))
)
// getFruit : Effect<string> <-- OutOfMango is GONE from the error line
Look at what happened to the type. Before: Effect<string, OutOfMango>. After: Effect<string>.
The compiler saw that we wrote a plan for OutOfMango and crossed it off the card.
If a card still has errors on it when you reach Effect.runPromise, that’s your reminder that
you haven’t planned for them.
Effect.catch (no tag) is the “catch anything on the error line” version. Notice it still doesn’t
catch defects. Fires are not on the error line.
Two tools that snap onto any card
Because a card is just a plan, you can wrap a plan around a plan. Two favorites:
Retry. “If it fails, try again, up to 3 more times.”
const stubbornMango = Effect.retry(getMango, { times: 3 })
Timeout. “Give up if it takes longer than 2 seconds.” The card’s error line grows: now it can also fail with a timeout error.
const quickMango = Effect.timeout(getMango, "2 seconds")
// quickMango : Effect<string, OutOfMango | TimeoutError>
Try doing that to a Promise. You can’t wrap a plan around a receipt, because the receipt already started.
flowchart LR
G[getMango] --> R["retry(times: 3)"] --> T["timeout(2 seconds)"] --> C["catchTag(OutOfMango → 🍓)"] --> Out[a new card]
Each box is still just a card. Nothing has run.
Check yourself
- “Out of mango” versus “kitchen fire”: which one belongs on the recipe card, and why?
- Before
catchTag, the type wasEffect<string, OutOfMango>. After, it wasEffect<string>. Who crossed the error off, and how did they know they could? - Why can you wrap
retryaround an Effect but not around a Promise? Effect.timeoutadds an error to the card. Does that seem like a bad thing? (No: it’s being honest.)
Teacher notes
- The single most important moment in this unit is showing the error type disappear when
you add
catchTag. Do it live with hover-over types in the editor. Let them see the red squiggle when they forget. Data.TaggedError("Name")makes a class with a_tagfield. The_tagis literally the name tag. You canconsole.log(err._tag)to prove it.- Defects come from things like
throwinside plain code, orEffect.die. You can catch them withEffect.catchDefect, but tell students that’s for the “fire alarm” layer of the program, not recipes. Effect.retryaccepts aScheduletoo (Schedule.exponential("100 millis"), for instance), which is “wait a bit longer between each try.” Mention it, don’t teach it.- Ask students to write down the “Can go wrong” line for something from their own lives: logging into a game (WrongPassword, ServerDown, TooManyAttempts). This makes it click that errors are a list of known things, not “stuff happened.”