Resource Management
Resource Management in Effect
Overview
Effect provides structured resource management that guarantees cleanup even when errors occur or the effect is interrupted. This is essential for:
- Database connections
- File handles
- Network sockets
- Locks and semaphores
- Any resource requiring cleanup
Core Concept: Scope
A Scope is a context that tracks resources and ensures their cleanup:
Effect<A, E, R | Scope>
// ^^^^^ Indicates resource needs cleanup
Basic Resource Acquisition
Effect.acquireRelease
The fundamental pattern for safe resource management:
import { Effect } from "effect"
const managedFile = Effect.acquireRelease(
Effect.sync(() => fs.openSync("file.txt", "r")),
(fd) => Effect.sync(() => fs.closeSync(fd))
)
Using the Resource
const program = Effect.gen(function* () {
const fd = yield* managedFile
const content = yield* Effect.sync(() => fs.readFileSync(fd, "utf-8"))
return content
})
// Run with automatic scope management
const result = yield* Effect.scoped(program)
Effect.scoped
Converts a scoped effect into a regular effect by managing the scope:
const runnable = Effect.scoped(program)
The scope closes when the scoped block completes, triggering all finalizers.
acquireUseRelease Pattern
For simpler cases, combine acquire/use/release in one call:
const readFile = (path: string) =>
Effect.acquireUseRelease(
Effect.sync(() => fs.openSync(path, "r")),
(fd) => Effect.sync(() => fs.readFileSync(fd, "utf-8")),
(fd) => Effect.sync(() => fs.closeSync(fd))
)
Finalizers
Effect.addFinalizer
Add cleanup logic to the current scope:
const program = Effect.gen(function* () {
yield* Effect.addFinalizer(() =>
Effect.log("Cleanup running!")
)
// ... do work ...
return result
})
Effect.ensuring
Run cleanup after effect completes (success or failure):
const withCleanup = someEffect.pipe(
Effect.ensuring(
Effect.log("Always runs after effect")
)
)
Effect.onExit
Run different cleanup based on exit status:
const withExitHandler = someEffect.pipe(
Effect.onExit((exit) =>
Exit.isSuccess(exit)
? Effect.log("Succeeded!")
: Effect.log("Failed or interrupted")
)
)
Multiple Resources
Sequential Acquisition
const program = Effect.gen(function* () {
const db = yield* acquireDbConnection
const cache = yield* acquireRedisConnection
})
const result = yield* Effect.scoped(program)
Parallel Acquisition
const program = Effect.gen(function* () {
const [db, cache] = yield* Effect.all([
acquireDbConnection,
acquireRedisConnection
])
})
Resource Patterns
Database Connection Pool
const DbPool = Effect.acquireRelease(
Effect.promise(() => createPool({
host: "localhost",
database: "mydb",
max: 10
})),
(pool) => Effect.promise(() => pool.end())
)
const query = (sql: string) =>
Effect.gen(function* () {
const pool = yield* DbPool
return yield* Effect.tryPromise(() => pool.query(sql))
})
File Handle
const withFile = <A>(
path: string,
use: (handle: FileHandle) => Effect.Effect<A>
) =>
Effect.acquireUseRelease(
Effect.promise(() => fs.promises.open(path)),
use,
(handle) => Effect.promise(() => handle.close())
)
Lock/Mutex
const withLock = <A>(
lock: Lock,
effect: Effect.Effect<A>
) =>
Effect.acquireUseRelease(
lock.acquire,
() => effect,
() => lock.release
)
Layered Resources
Use Layer.scoped for service-level resources:
const DatabaseLive = Layer.scoped(
Database,
Effect.gen(function* () {
const pool = yield* Effect.acquireRelease(
createPool(),
(pool) => Effect.promise(() => pool.end())
)
return {
query: (sql) => Effect.tryPromise(() => pool.query(sql))
}
})
)
Error Handling in Cleanup
Finalizers should not fail, but if they do:
const safeRelease = (resource: Resource) =>
Effect.sync(() => resource.close()).pipe(
Effect.catchAll((error) =>
Effect.logError("Cleanup failed", error)
)
)
const managed = Effect.acquireRelease(
acquire,
safeRelease
)
Interruption Safety
Resources are cleaned up even on interruption:
const program = Effect.gen(function* () {
const resource = yield* acquireResource
yield* Effect.sleep("1 hour")
})
const result = yield* program.pipe(
Effect.scoped,
Effect.timeout("1 second")
)
Best Practices
- Use acquireRelease for paired operations - Guarantees cleanup
- Keep finalizers simple and infallible - Log errors instead of throwing
- Use Effect.scoped at appropriate boundaries - Not too wide, not too narrow
- Clean up in reverse acquisition order - Effect handles this automatically
- Use Layer.scoped for service-level resources - Lifecycle tied to layer
Additional Resources
For comprehensive resource management documentation, consult ${CLAUDE_PLUGIN_ROOT}/references/llms-full.txt.
Search for these sections:
- "Introduction" (Resource Management) for core concepts
- "Scope" for detailed scope mechanics
- "Managing Layers" for Layer.scoped patterns