Scope API.TL;DR (Quick Summary)#
- The Problem: Native
try/finallyblocks are notoriously brittle in asynchronous JavaScript. If a Promise hangs or a Fiber is interrupted, thefinallyblock might never execute. - The Solution: The
ScopeAPI is an Effect requirement (it addsScopeto theRchannel) that guarantees resource cleanup. Effect.acquireRelease: The core function. You provide an Acquire effect (e.g., connect to DB) and a Release effect (e.g., disconnect). Effect-TS guarantees the Release effect will run.- Execution: To run a scoped program, you wrap it in
Effect.scoped(), which creates a boundary. When the boundary exits (via success, failure, or interruption), all resources are destroyed.
1. Introduction: The Resource Leak Problem#
A “Resource” is any entity that has a lifecycle: it must be opened, and it must be closed. Examples include:
- Database Connections (e.g., Postgres Pools)
- File Streams
- WebSocket Connections
- Background Timers
In standard TypeScript, we manage resources using try/finally.
// 🔴 The Native TypeScript Way
async function processFile(path: string) {
const file = await openFileStream(path); // Acquire
try {
await performMassiveComputation(file); // Use
} finally {
await file.close(); // Release
}
}This looks safe, but it is deeply flawed in highly concurrent environments.
What if processFile is running, but the user cancels the HTTP request? As we learned in Episode 67, native Promises cannot be interrupted. If we build our own interruption logic, the finally block is often completely bypassed, leaving the file permanently open in the OS kernel.
2. Introducing Effect.acquireRelease#
Effect-TS abandons try/finally. Instead, it forces you to define exactly how to acquire a resource and exactly how to release it in one atomic step.
import { Effect } from "effect";
// 🟢 Step 1: Define the Acquire and Release logic
const getDatabaseConnection = Effect.acquireRelease(
// 1. The Acquire Effect
Effect.sync(() => {
console.log("Acquiring database connection...");
return { id: "conn_123", close: () => console.log("Closing connection...") };
}),
// 2. The Release Effect
(connection, exit) => Effect.sync(() => {
// This runs NO MATTER WHAT.
connection.close();
console.log(`Resource closed because the pipeline exited with: ${exit._tag}`);
})
);If you hover over getDatabaseConnection, you will see a fascinating type signature:
Effect.Effect<{ id: string; close: () => void }, never, Scope>Notice that Scope has been automatically injected into the R (Requirements) channel!
3. Step-by-Step: Using the Resource#
Once you have defined the resource, you can use it in your pipeline using Effect.flatMap.
const program = Effect.pipe(
// 1. Acquire the resource
getDatabaseConnection,
// 2. Use the resource
Effect.flatMap(connection => Effect.sync(() => {
console.log(`Executing query using ${connection.id}...`);
// Simulate a fatal crash!
throw new Error("BOOM! Database Crash!");
}))
);In the pipeline above, our business logic throws a fatal exception. If we run this program, what happens to our connection?
4. Closing the Scope Boundary#
If we try to run our program using Effect.runPromise(program), the compiler will yell at us:
TS2345: Argument of type 'Effect<void, never, Scope>' is not assignable...
Because the R channel contains Scope, we must provide a Scope! We do this by wrapping the entire program in Effect.scoped.
Effect.scoped creates a boundary. As soon as the pipeline finishes (or crashes) within that boundary, the Scope is destroyed, and the Release functions are executed in reverse order.
// 🟢 Create the execution boundary
const safeProgram = Effect.scoped(program);
// Execute it!
Effect.runPromise(safeProgram).catch(() => console.error("Pipeline failed."));
// Output:
// Acquiring database connection...
// Executing query using conn_123...
// Closing connection...
// Resource closed because the pipeline exited with: Failure
// Pipeline failed.
Notice that even though the business logic threw a fatal BOOM! error, Effect caught the crash, interrupted the Fiber, and executed the Release function perfectly. No memory leaks!
5. Comparison: try/finally vs Scope#
| Feature | try/finally | Effect.acquireRelease |
|---|---|---|
| Composability | Poor. Cannot easily pass a try block across files. | Excellent. Resources are just normal Effect values that can be passed anywhere. |
| Interruption Safety | Dangerous. Can easily bypass finally. | 100% Mathematically guaranteed to execute on interruption. |
| Multiple Resources | Creates deep, nested “Pyramids of Doom”. | Trivial. Just pipe multiple acquireRelease calls together. |
| Type Tracking | None. | Compiler forces you to use Effect.scoped. |
6. Troubleshooting & Common Errors#
Error 1: Forgetting Effect.scoped#
TS2345: Argument of type 'Effect<A, E, Scope>' is not assignable to parameter of type 'Effect<A, E, never>'.The Cause: You built an amazing pipeline using a scoped resource, but you forgot to close the boundary using Effect.scoped. The runtime refuses to execute because it doesn’t know when to release the resource.
The Fix: Wrap the outermost layer of your application logic in Effect.scoped(...).
Error 2: Releasing Asynchronous Resources Unsafely#
The Mistake: Your Release function needs to execute an asynchronous Promise (like a graceful HTTP disconnect), but you wrapped it in Effect.sync.
The Fix: The Release function in acquireRelease expects an Effect. You can use Effect.promise or Effect.tryPromise directly inside the release block!
Effect.acquireRelease(
connectToWebSocket,
(ws) => Effect.promise(() => ws.gracefulDisconnectAsync()) // 🟢 Perfectly safe!
)Summary & Next Steps#
In this episode:
- We identified the dangers of native
try/finallyblocks in highly concurrent architectures. - We used
Effect.acquireReleaseto bind an initialization step directly to its destruction step. - We observed how the
Scoperequirement is pushed into theRchannel. - We used
Effect.scopedto define the exact boundary where resources should be destroyed.
We have now learned every core primitive of Effect-TS. We understand types, errors, dependencies, layers, concurrency, validation, and resources.
It is time for the Grand Finale.
In Episode 70: Building a REST API with Effect, we will combine everything we have learned to build a production-grade, fully functional Express backend!

