Overview
Master automatic resource management in Effect using Scopes and finalizers. This skill covers resource acquisition, cleanup, scoped effects, and patterns for building leak-free Effect applications.
Scope Fundamentals
A Scope represents the lifetime of resources. When a scope closes, all registered finalizers execute automatically.
Basic Scope Usage
import { Effect, Scope } from "effect"
const program = Effect.scoped(
Effect.gen(function* () {
// Resources acquired here are tied to this scope
const resource = yield* acquireResource()
// Use resource
const result = yield* useResource(resource)
return result
// Scope closes here, resources cleaned up automatically
})
)
Adding Finalizers
import { Effect } from "effect"
const acquireFile = (path: string) =>
Effect.gen(function* () {
// Acquire resource
const file = yield* Effect.sync(() => openFile(path))
// Register cleanup
yield* Effect.addFinalizer(() =>
Effect.sync(() => {
console.log(`Closing file: ${path}`)
file.close()
})
)
return file
})
// Usage
const program = Effect.scoped(
Effect.gen(function* () {
const file = yield* acquireFile("data.txt")
const content = yield* readFile(file)
return content
// File automatically closed on scope exit
})
)
Finalizer Behavior
Execution Order
Finalizers execute in reverse order of registration (LIFO):
import { Effect } from "effect"
const program = Effect.scoped(
Effect.gen(function* () {
yield* Effect.addFinalizer(() =>
Effect.log("Finalizer 1")
)
yield* Effect.addFinalizer(() =>
Effect.log("Finalizer 2")
)
yield* Effect.addFinalizer(() =>
Effect.log("Finalizer 3")
)
return "done"
})
)
// Output:
// Finalizer 3
// Finalizer 2
// Finalizer 1
Exit Information
Finalizers receive exit information:
import { Effect, Exit } from "effect"
const acquireWithContext = Effect.gen(function* () {
yield* Effect.addFinalizer((exit) =>
Effect.sync(() => {
if (Exit.isSuccess(exit)) {
console.log("Scope exited successfully:", exit.value)
} else if (Exit.isFailure(exit)) {
console.log("Scope failed:", exit.cause)
} else {
console.log("Scope interrupted")
}
})
)
// Acquire resource
const resource = yield* Effect.sync(() => createResource())
return resource
})
Resource Patterns
Database Connection
import { Effect } from "effect"
interface DbConnection {
query: <T>(sql: string) => Promise<T>
close: () => Promise<void>
}
const acquireConnection = (config: DbConfig) =>
Effect.gen(function* () {
// Acquire connection
const conn = yield* Effect.tryPromise({
try: () => createConnection(config),
catch: (error) => ({
_tag: "ConnectionError",
message: String(error)
})
})
// Register cleanup
yield* Effect.addFinalizer(() =>
Effect.tryPromise({
try: () => conn.close(),
catch: (error) => ({
_tag: "CloseError",
message: String(error)
})
}).pipe(
Effect.catchAll((error) =>
Effect.log(`Failed to close connection: ${error.message}`)
)
)
)
return conn
})
// Usage
const queryDatabase = Effect.scoped(
Effect.gen(function* () {
const conn = yield* acquireConnection(dbConfig)
const users = yield* Effect.tryPromise(() =>
conn.query<User[]>("SELECT * FROM users")
)
return users
// Connection automatically closed
})
)
File Operations
import { Effect } from "effect"
import * as fs from "fs/promises"
const withFile = <A, E, R>(
path: string,
use: (handle: fs.FileHandle) => Effect.Effect<A, E, R>
) =>
Effect.scoped(
Effect.gen(function* () {
// Acquire file handle
const handle = yield* Effect.tryPromise({
try: () => fs.open(path, "r"),
catch: (error) => ({
_tag: "FileError",
message: String(error)
})
})
// Register cleanup
yield* Effect.addFinalizer(() =>
Effect.tryPromise(() => handle.close()).pipe(
Effect.catchAll(() => Effect.void)
)
)
// Use file
return yield* use(handle)
})
)
// Usage
const readFileContent = withFile("data.txt", (handle) =>
Effect.tryPromise(() => handle.readFile({ encoding: "utf8" }))
)
Network Resources
import { Effect } from "effect"
interface WebSocket {
send: (data: string) => void
close: () => void
onMessage: (handler: (data: string) => void) => void
}
const acquireWebSocket = (url: string) =>
Effect.gen(function* () {
const ws = yield* Effect.async<WebSocket, never>((resume) => {
const socket = new WebSocket(url)
socket.onopen = () => {
resume(Effect.succeed(socket))
}
socket.onerror = () => {
resume(Effect.fail({ _tag: "ConnectionError" }))
}
})
yield* Effect.addFinalizer(() =>
Effect.sync(() => {
console.log("Closing WebSocket")
ws.close()
})
)
return ws
})
Scoped Effects
Effect.acquireRelease
Simplified resource acquisition:
import { Effect } from "effect"
const resource = Effect.acquireRelease(
// Acquire
Effect.sync(() => {
console.log("Acquiring resource")
return createResource()
}),
// Release
(resource) =>
Effect.sync(() => {
console.log("Releasing resource")
resource.cleanup()
})
)
// Usage
const program = Effect.scoped(
Effect.gen(function* () {
const r = yield* resource
return yield* useResource(r)
})
)
Effect.acquireUseRelease
One-shot resource usage:
import { Effect } from "effect"
const readConfig = Effect.acquireUseRelease(
// Acquire
Effect.tryPromise(() => fs.open("config.json", "r")),
// Use
(handle) =>
Effect.tryPromise(() =>
handle.readFile({ encoding: "utf8" })
).pipe(
Effect.map((content) => JSON.parse(content))
),
// Release
(handle) =>
Effect.tryPromise(() => handle.close()).pipe(
Effect.orDie
)
)
Nested Scopes
Scope Nesting
Scopes can be nested for hierarchical cleanup:
import { Effect } from "effect"
const program = Effect.scoped(
Effect.gen(function* () {
const db = yield* acquireConnection()
yield* Effect.scoped(
Effect.gen(function* () {
const transaction = yield* beginTransaction(db)
yield* updateUsers(transaction)
yield* commitTransaction(transaction)
// Transaction scope ends, resources cleaned up
})
)
// DB connection still alive
yield* runQuery(db)
// DB scope ends, connection closed
})
)
Parallel Scopes
import { Effect } from "effect"
const parallelResources = Effect.gen(function* () {
const results = yield* Effect.all([
Effect.scoped(
Effect.gen(function* () {
const conn1 = yield* acquireConnection(db1Config)
return yield* queryDb(conn1)
})
),
Effect.scoped(
Effect.gen(function* () {
const conn2 = yield* acquireConnection(db2Config)
return yield* queryDb(conn2)
})
)
])
return results
// Both connections closed automatically
})
Advanced Patterns
Resource Pool
import { Effect, Queue, Ref } from "effect"
interface Pool<R> {
acquire: Effect.Effect<R, never, Scope.Scope>
release: (resource: R) => Effect.Effect<void, never, never>
}
const createPool = <R, E>(
create: Effect.Effect<R, E, never>,
destroy: (resource: R) => Effect.Effect<void, never, never>,
size: number
): Effect.Effect<Pool<R>, E, Scope.Scope> =>
Effect.gen(function* () {
const available = yield* Queue.bounded<R>(size)
const counter = yield* Ref.make(0)
// Initialize pool
yield* Effect.forEach(
Array.from({ length: size }),
() =>
Effect.gen(function* () {
const resource = yield* create
yield* Queue.offer(available, resource)
}),
{ concurrency: "unbounded" }
)
// Register pool cleanup
yield* Effect.addFinalizer(() =>
Effect.gen(function* () {
const resources = yield* Queue.takeAll(available)
yield* Effect.forEach(
resources,
(r) => destroy(r),
{ concurrency: "unbounded" }
)
})
)
return {
acquire: Effect.gen(function* () {
const resource = yield* Queue.take(available)
yield* Effect.addFinalizer(() => Queue.offer(available, resource))
return resource
}),
release: (resource) => Queue.offer(available, resource)
}
})
Cached Resource
import { Effect, Ref } from "effect"
const cached = <A, E, R>(
acquire: Effect.Effect<A, E, R>
): Effect.Effect<Effect.Effect<A, E, never>, never, Scope.Scope | R> =>
Effect.gen(function* () {
const ref = yield* Ref.make<Option<A>>(Option.none())
yield* Effect.addFinalizer(() =>
ref.set(Option.none())
)
return ref.get.pipe(
Effect.flatMap((option) =>
Option.match(option, {
onNone: () =>
acquire.pipe(
Effect.tap((value) => ref.set(Option.some(value)))
),
onSome: (value) => Effect.succeed(value)
})
)
)
})
Best Practices
-
Always Use Scoped: Acquire resources within Effect.scoped.
-
Register Finalizers Immediately: Add finalizers right after acquisition.
-
Handle Cleanup Errors: Catch and log errors in finalizers.
-
Reverse Order: Rely on LIFO finalizer execution for dependencies.
-
Use acquireRelease: Prefer acquireRelease for simple acquire/release patterns.
-
Test Cleanup: Verify finalizers execute correctly.
-
Avoid Manual Cleanup: Don't manually clean up scoped resources.
-
Nest Appropriately: Use nested scopes for hierarchical resources.
-
Pool Expensive Resources: Use resource pools for expensive acquisitions.
-
Document Scope Requirements: Make it clear which effects need scopes.
Common Pitfalls
-
Missing Scoped: Acquiring resources without Effect.scoped.
-
Not Adding Finalizers: Forgetting to register cleanup.
-
Finalizer Errors: Throwing errors in finalizers without handling.
-
Wrong Scope Nesting: Closing scopes in wrong order.
-
Resource Leaks: Not cleaning up on all exit paths.
-
Duplicate Cleanup: Cleaning up resources multiple times.
-
Blocking Finalizers: Using long-running operations in finalizers.
-
Ignoring Exit Info: Not using exit information appropriately.
-
Scope Scope Confusion: Confusing when scopes close.
-
Missing Error Handling: Not handling errors during acquisition.
When to Use This Skill
Use effect-resource-management when you need to:
- Manage database connections
- Handle file operations safely
- Work with network resources
- Implement connection pools
- Build transaction systems
- Ensure cleanup on all exit paths
- Manage WebSocket connections
- Handle distributed locks
- Implement caching with cleanup
- Build leak-free applications
Resources
Official Documentation
Related Skills
- effect-core-patterns - Basic Effect operations
- effect-concurrency - Managing fiber lifecycles
- effect-dependency-injection - Layer cleanup with scoped