Skill v1.0.1
currentAutomated scan100/1004 files
version: "1.0.1" name: effect-resource-management user-invocable: false description: Use when Effect resource management patterns including Scope, addFinalizer, scoped effects, and automatic cleanup. Use for managing resources in Effect applications. allowed-tools:
- Bash
- Read
- Write
- Edit
Effect Resource Management
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 scopeconst resource = yield* acquireResource()// Use resourceconst 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 resourceconst file = yield* Effect.sync(() => openFile(path))// Register cleanupyield* Effect.addFinalizer(() =>Effect.sync(() => {console.log(`Closing file: ${path}`)file.close()}))return file})// Usageconst 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 resourceconst 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 connectionconst conn = yield* Effect.tryPromise({try: () => createConnection(config),catch: (error) => ({_tag: "ConnectionError",message: String(error)})})// Register cleanupyield* 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})// Usageconst 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 handleconst handle = yield* Effect.tryPromise({try: () => fs.open(path, "r"),catch: (error) => ({_tag: "FileError",message: String(error)})})// Register cleanupyield* Effect.addFinalizer(() =>Effect.tryPromise(() => handle.close()).pipe(Effect.catchAll(() => Effect.void)))// Use filereturn yield* use(handle)}))// Usageconst readFileContent = withFile("data.txt", (handle) =>Effect.tryPromise(() => handle.readFile({ encoding: "utf8" })))
Network Resources
import { Effect } from "effect"interface WebSocket {send: (data: string) => voidclose: () => voidonMessage: (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(// AcquireEffect.sync(() => {console.log("Acquiring resource")return createResource()}),// Release(resource) =>Effect.sync(() => {console.log("Releasing resource")resource.cleanup()}))// Usageconst program = Effect.scoped(Effect.gen(function* () {const r = yield* resourcereturn yield* useResource(r)}))
Effect.acquireUseRelease
One-shot resource usage:
import { Effect } from "effect"const readConfig = Effect.acquireUseRelease(// AcquireEffect.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 aliveyield* 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 poolyield* Effect.forEach(Array.from({ length: size }),() =>Effect.gen(function* () {const resource = yield* createyield* Queue.offer(available, resource)}),{ concurrency: "unbounded" })// Register pool cleanupyield* 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