Lesson 7: Cleaning Up
The big idea
Whatever you borrow, you give back, even when things go wrong. Effect has a built-in rule for “get a thing, use it, always put it back,” and it holds no matter how the recipe ends: success, error, or interruption.
The story
You check a book out of the library. Three ways your afternoon can go:
- You read it and return it.
- You spill juice on chapter three, and you still have to return it.
- Your parent calls and says “come home NOW,” and you still have to return it.
The return is not optional. It doesn’t care why you stopped reading.
Kitchen version: you turn on the blender. Whether the smoothie succeeds, the fruit was rotten, or the customer left and the recipe was interrupted, the blender gets turned off. A blender left running overnight is how kitchens burn down.
Effect calls this pattern acquire and release, and the guarantee is: release always runs.
The picture
flowchart TD
A["acquire: turn blender ON"] --> U["use: blend the fruit"]
U -- success 🥤 --> R["release: turn blender OFF"]
U -- "error (rotten fruit)" --> R
U -- "interrupted (customer left)" --> R
R --> Done[done, blender is OFF no matter what]
style R fill:#dfe,stroke:#393
Every arrow out of “use” goes through “release.” There is no path that skips it.
The tiniest code
import { Effect, Console } from "effect"
const blenderOn = Console.log("blender ON 🔛")
const blenderOff = Console.log("blender OFF ⏹️")
const useBlender = Effect.acquireRelease(
blenderOn, // acquire
() => blenderOff // release: ALWAYS runs
)
const smoothie = Effect.gen(function* () {
yield* useBlender
yield* Console.log("blending...")
yield* Effect.fail(new Error("rotten fruit!")) // uh oh
yield* Console.log("pouring") // never reached
})
Effect.runPromise(Effect.scoped(smoothie)).catch(() => {})
// blender ON 🔛
// blending...
// blender OFF ⏹️ <-- ran anyway
Delete the Effect.fail line and you’ll see ON, blending, pouring, OFF. Put it back: ON, blending, OFF.
Either way, OFF.
What’s Effect.scoped?
The library book has a due date: how long you’re allowed to keep it. Effect calls that window a
scope. Effect.acquireRelease adds “Needs: a Scope” to the card’s third line, because the card
needs to know when to return things. Effect.scoped says “the scope is this recipe: return everything
when it ends.” Layers (Lesson 5) also make scopes, so a blender acquired in a layer is returned when the
app shuts down.
You do not need to understand scopes deeply this month. You need: acquireRelease borrows, scoped sets the due date, release always happens.
Why this is a big deal
With a Promise, there’s no “interrupted” path at all, so people forget cleanup on the “customer left” case, because that case doesn’t exist for them. Then a real program runs for a week and has ten thousand blenders still on. Effect makes the interrupted path a first-class thing, and cleanup runs on it too.
Check yourself
- Name the three ways a recipe can end. Which of them skips the release step?
- Why is “turn the blender off” not just the last line of the recipe? (Hint: what if line 3 fails?)
- In library words, what’s a scope?
- A friend says “I’ll just put cleanup in the
.catchof my Promise.” What case did they forget?
Teacher notes
acquireRelease(acquire, release)returns the acquired thing, so you can doconst blender = yield* useBlenderand then use it. The example throws it away for simplicity.Effect.ensuring(cleanup)is the simpler cousin: “after this card, always run cleanup.”Effect.addFinalizerregisters cleanup from inside agenblock. Both are fine to mention.- A nice closer for the unit: draw the full smoothie card on the board with all three lines filled
in, a
retryandtimeoutwrapped around it, aBlenderlayer provided, andacquireReleasekeeping the blender safe. Then ask: “What can go wrong that we haven’t written down?” The honest answer, “a defect, and we’d see it loudly,” is the whole philosophy in one sentence.