Loading learning content...
Every resource has a lifecycle—a predictable journey from creation to destruction. Understanding this lifecycle is fundamental to resource management because correct management means honoring the lifecycle invariants at every phase.
The lifecycle concept is universal. Whether you're dealing with a file handle, a database connection, a GPU texture, or a distributed lock, the same abstract phases apply. Mastering the lifecycle means you can reason about any resource, even ones you've never encountered before.
This page provides a detailed examination of each lifecycle phase, the state transitions between them, and the patterns that ensure resources are managed correctly throughout their existence.
By the end of this page, you will understand: (1) The three primary lifecycle phases and their responsibilities, (2) Valid and invalid state transitions, (3) Error handling at each phase, (4) The role of ownership in lifecycle management, and (5) Common lifecycle anti-patterns to avoid.
At its core, every resource lifecycle consists of three phases:
Phase 1: Acquisition (Creation/Opening)
The resource is obtained from the external system. This phase:
Phase 2: Usage (Active)
The resource is actively used for its intended purpose. This phase:
Phase 3: Release (Disposal/Closing)
The resource is returned to the provider. This phase:
┌─────────────────────────────────────────────────────────────────────┐
│ RESOURCE LIFECYCLE │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ │ │ │ │ │ │
│ │ ACQUISITION │─────►│ USAGE │─────►│ RELEASE │ │
│ │ │ │ │ │ │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ • Request resource • Perform operations • Flush buffers │
│ • Authenticate • Handle errors • Notify provider │
│ • Negotiate • Respect semantics • Free memory │
│ • Handle failure • Maintain validity • Return to pool │
│ │
└─────────────────────────────────────────────────────────────────────┘
The fundamental lifecycle invariant: Usage may only occur between Acquisition and Release. Every use of a resource must be preceded by successful acquisition and followed (eventually) by release. Any violation—use before acquire, use after release, or missing release—is a bug.
The acquisition phase establishes the resource and determines whether its use is possible. This phase carries significant complexity because acquisition can fail in numerous ways, and each failure mode requires different handling.
Acquisition patterns:
1. Direct Acquisition
You explicitly request the resource:
open(path, mode) → file handlenew Connection(config) → connection objectlock.acquire() → lock grantDirect acquisition gives you full control but full responsibility.
2. Pool-Based Acquisition
You borrow from a pre-created pool:
pool.getConnection() → borrowed connectionpool.acquire() → borrowed workerPool acquisition may block if pool is exhausted, or fail with timeout.
3. Lazy Acquisition
Resource acquired on first use:
Lazy acquisition simplifies client code but complicates error handling—acquisition errors occur at unexpected points.
4. Factory Acquisition
A factory method encapsulates acquisition logic:
connectionFactory.createConnection()resourceManager.getOrCreate(key)Factories can embed retry logic, configuration, and resource tracking.
| Failure Mode | Example | Typical Handling |
|---|---|---|
| Resource not found | File doesn't exist | Report error, don't retry |
| Permission denied | Access control blocks | Report error, escalate if needed |
| Limit exceeded | Too many connections | Retry with backoff, or fail |
| Timeout | Connection SYN times out | Retry with backoff |
| Service unavailable | Database down | Retry, use fallback, or fail |
| Invalid parameters | Bad connection string | Report error, check config |
| Authentication failed | Wrong credentials | Report error, check secrets |
| Resource conflict | Exclusive lock held | Retry or abort depending on use case |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
// Direct acquisition with error handlingasync function directAcquisition(path: string): Promise<FileHandle> { try { const file = await open(path, 'r'); return file; } catch (error) { if (error.code === 'ENOENT') { throw new ResourceNotFoundError(`File not found: ${path}`); } if (error.code === 'EACCES') { throw new AccessDeniedError(`Permission denied: ${path}`); } if (error.code === 'EMFILE') { throw new ResourceExhaustedError('Too many open files'); } throw error; // Unknown error - propagate }} // Pool-based acquisition with timeoutasync function poolAcquisition(pool: ConnectionPool): Promise<PooledConnection> { const timeout = 5000; // 5 seconds try { const connection = await pool.acquire({ timeout }); return connection; } catch (error) { if (error.code === 'POOL_TIMEOUT') { // Log for diagnostics - pool exhaustion is a serious issue console.error('Connection pool exhausted'); throw new ServiceUnavailableError('Database pool exhausted'); } throw error; }} // Factory with retry logicclass ConnectionFactory { async createConnection(config: ConnectionConfig): Promise<Connection> { const maxRetries = 3; const baseDelay = 100; for (let attempt = 1; attempt <= maxRetries; attempt++) { try { const connection = await this.doConnect(config); return connection; } catch (error) { if (attempt === maxRetries || !this.isRetryable(error)) { throw error; } const delay = baseDelay * Math.pow(2, attempt - 1); await this.sleep(delay); } } throw new Error('Unreachable'); } private isRetryable(error: Error): boolean { // Network errors and temporary unavailability are retryable return error.code === 'ECONNREFUSED' || error.code === 'ETIMEDOUT' || error.code === 'ECONNRESET'; }}Once acquisition succeeds, you are responsible for eventual release. This responsibility begins immediately—even if the next line of code throws an exception. This is why try-finally patterns are essential: success at acquisition mandates release logic.
The usage phase is where resources provide value. During this phase, you perform the operations the resource enables. But usage isn't just 'use freely'—it has rules, constraints, and error handling requirements.
Usage phase responsibilities:
1. Validity Checking
Before every operation, ensure the resource is still valid:
2. Error Handling
Operations can fail during usage:
Error handling during usage must not prevent release—cleanup must still occur.
3. State Management
Resources have state that changes during usage:
You must track state and ensure consistency.
4. Concurrency Rules
Many resources have thread-safety rules:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485
class ConnectionWrapper { private connection: Connection | null = null; private transactionActive: boolean = false; // Check validity before every operation private ensureValid(): void { if (this.connection === null) { throw new InvalidStateError('Connection not acquired'); } if (!this.connection.isAlive()) { throw new ConnectionLostError('Connection is no longer active'); } } async query(sql: string, params: any[]): Promise<Result> { this.ensureValid(); try { return await this.connection!.query(sql, params); } catch (error) { // Operation failed - connection may be corrupted if (this.isConnectionError(error)) { // Mark connection as invalid this.connection!.markBroken(); } throw error; } } async beginTransaction(): Promise<void> { this.ensureValid(); if (this.transactionActive) { throw new InvalidStateError('Transaction already active'); } await this.connection!.query('BEGIN'); this.transactionActive = true; } async commit(): Promise<void> { this.ensureValid(); if (!this.transactionActive) { throw new InvalidStateError('No active transaction'); } try { await this.connection!.query('COMMIT'); } finally { this.transactionActive = false; } } async rollback(): Promise<void> { this.ensureValid(); if (!this.transactionActive) { throw new InvalidStateError('No active transaction'); } try { await this.connection!.query('ROLLBACK'); } finally { this.transactionActive = false; } } // Transaction state affects cleanup - must rollback if active async release(): Promise<void> { if (this.connection) { if (this.transactionActive) { try { await this.connection.query('ROLLBACK'); } catch (error) { // Log but continue - we must release console.error('Rollback failed during release', error); } } await this.connection.close(); this.connection = null; } }}Cascading resource usage:
Resources often depend on other resources during usage. A database transaction may:
These 'child resources' create nested cleanup obligations. Properly structured code handles these cascades automatically through compositional patterns.
Usage timeout considerations:
Long-running operations during usage can hold resources indefinitely:
Timeouts during usage protect against resource starvation but require careful handling—you may need to abort operations or force cleanup.
The release phase completes the lifecycle by returning the resource to the provider. This phase is non-optional—skipping it causes leaks. Yet release is also the phase most frequently botched, especially in error paths.
Release phase responsibilities:
1. Flush Pending Operations
Buffered data must be written before the resource is released:
2. Protocol Compliance
Clean shutdown often requires protocol-specific steps:
3. Cleanup of Derived Resources
Releasing a parent resource requires releasing all child resources:
| Pattern | Mechanism | Best For |
|---|---|---|
| Explicit close | resource.close() | Simple, clear ownership |
| Using/try-with-resources | Language construct | Scoped usage, automatic cleanup |
| Context manager | Python with statement | Pythonic resource handling |
| Pool return | pool.release(resource) | Pool-managed resources |
| Factory cleanup | factory.destroy(resource) | Factory-created resources |
| Scope-based (RAII) | Destructor on scope exit | C++, Rust |
| Finalizer/destructor | GC-triggered cleanup | Last resort only |
123456789101112131415161718192021222324252627282930313233343536373839404142434445
// Pattern 1: Explicit try/finallyasync function explicitRelease(path: string): Promise<string> { const file = await open(path, 'r'); try { return await file.readFile('utf-8'); } finally { await file.close(); }} // Pattern 2: Using-style helper (Symbol.asyncDispose)async function usingRelease(path: string): Promise<string> { await using file = await open(path, 'r'); // file.close() called automatically when block exits return file.readFile('utf-8');} // Pattern 3: Wrapper that encapsulates cleanupasync function withFile<T>( path: string, operation: (file: FileHandle) => Promise<T>): Promise<T> { const file = await open(path, 'r'); try { return await operation(file); } finally { await file.close(); }} // Usage:const content = await withFile('data.txt', async (file) => { return file.readFile('utf-8');}); // Pattern 4: Pool release (not close!)async function poolRelease(pool: Pool): Promise<void> { const conn = await pool.acquire(); try { await conn.query('SELECT 1'); } finally { // Return to pool, don't close! pool.release(conn); }}Release failures are problematic because they occur in cleanup paths—often in finally blocks. If release throws, it may mask the original error. Best practice: log release failures but don't throw, or use specific exception handling that preserves the original error.
Resources can be modeled as state machines with well-defined states and transitions. Understanding the state machine helps prevent invalid operations and ensures correct lifecycle management.
Basic resource states:
┌─────────────────────────────────────────────────────────────────────┐
│ RESOURCE STATE MACHINE │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ │ │ │ │ │ │
│ │ UNOPEN │──────│ OPEN │──────│ CLOSED │ │
│ │ │ open │ │ close│ │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │ │
│ │ error │
│ ▼ │
│ ┌──────────┐ │
│ │ │ │
│ │ FAILED │ │
│ │ │ │
│ └──────────┘ │
│ │ │
│ │ close │
│ ▼ │
│ ┌──────────┐ │
│ │ │ │
│ │ CLOSED │ │
│ │ │ │
│ └──────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
Valid transitions:
Invalid transitions:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485
enum ResourceState { UNACQUIRED = 'UNACQUIRED', ACTIVE = 'ACTIVE', FAILED = 'FAILED', RELEASED = 'RELEASED',} class ManagedResource<T> { private state: ResourceState = ResourceState.UNACQUIRED; private resource: T | null = null; constructor( private acquire: () => Promise<T>, private release: (resource: T) => Promise<void> ) {} async open(): Promise<void> { // Validate transition if (this.state !== ResourceState.UNACQUIRED) { throw new InvalidStateError( `Cannot acquire from state ${this.state}` ); } try { this.resource = await this.acquire(); this.state = ResourceState.ACTIVE; } catch (error) { this.state = ResourceState.FAILED; throw error; } } async use<R>(operation: (resource: T) => Promise<R>): Promise<R> { // Validate state if (this.state !== ResourceState.ACTIVE) { throw new InvalidStateError( `Cannot use resource in state ${this.state}` ); } try { return await operation(this.resource!); } catch (error) { // Optionally mark as failed based on error type if (this.isFatalError(error)) { this.state = ResourceState.FAILED; } throw error; } } async close(): Promise<void> { // Allow closing from ACTIVE or FAILED states if (this.state === ResourceState.RELEASED) { return; // Idempotent - already closed } if (this.state === ResourceState.UNACQUIRED) { return; // Never opened - nothing to close } try { if (this.resource) { await this.release(this.resource); } } finally { this.resource = null; this.state = ResourceState.RELEASED; } } get isOpen(): boolean { return this.state === ResourceState.ACTIVE; } get isClosed(): boolean { return this.state === ResourceState.RELEASED; } private isFatalError(error: Error): boolean { // Connection reset, etc. make resource unusable return error.name === 'ConnectionResetError'; }}Extended states for complex resources:
Some resources have more complex state machines:
Connections:
Transactions:
Streams:
Modeling these states explicitly helps prevent invalid operations and provides clear documentation of expected behavior.
Ownership determines who is responsible for releasing a resource. Clear ownership is essential—confusion about ownership leads to either double-release (errors) or no release (leaks).
Ownership models:
1. Exclusive Ownership
Exactly one entity owns the resource:
This is the simplest and safest model.
2. Reference Counting
Multiple entities share ownership:
Used by smart pointers (shared_ptr), COM objects, and some language runtimes.
3. Borrowed References
One entity owns, others borrow:
4. Pool Ownership
Pool owns resources, clients borrow:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556
// Exclusive ownership - caller owns the resultasync function createExclusiveConnection(): Promise<Connection> { // Caller receives ownership - caller must close return await Connection.create(config);} // Usage - caller is responsibleasync function useExclusiveOwnership(): Promise<void> { const conn = await createExclusiveConnection(); try { // Use connection } finally { await conn.close(); // Our responsibility }} // Borrowed reference - callee uses, caller ownsasync function queryWithBorrowed( conn: Connection, // Borrowed - we don't own it sql: string): Promise<Result> { // Use connection, but don't close it return conn.query(sql); // No close() here - we don't own it} // Pool ownership - pool owns, we borrowasync function poolPattern(pool: ConnectionPool): Promise<void> { const conn = await pool.acquire(); // Borrow try { await conn.query('SELECT 1'); } finally { pool.release(conn); // Return borrow, don't close }} // Factory with ownership transferclass ConnectionFactory { create(): Connection { const conn = new Connection(); // Caller takes ownership - we're done return conn; } // Alternatively: managed creation with callback (no transfer) async withConnection<T>( operation: (conn: Connection) => Promise<T> ): Promise<T> { const conn = new Connection(); try { return await operation(conn); // Borrower uses } finally { await conn.close(); // We own, we release } }}Make ownership explicit in documentation and naming. Method names like createConnection() suggest caller takes ownership. Parameters named borrowedConn or return types like OwnedHandle make ownership clear. When in doubt, document who is responsible for release.
Certain patterns reliably cause lifecycle bugs. Recognizing these anti-patterns helps you avoid them in your own code and identify them in code review.
Anti-Pattern 1: The Forgotten Finally
Release code exists but isn't guaranteed to run:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364
// ❌ Anti-Pattern 1: Forgotten finallyasync function forgottenFinally(): Promise<void> { const conn = await pool.acquire(); const result = await conn.query('SELECT * FROM users'); if (result.rows.length === 0) { return; // Early return - release skipped! } processUsers(result.rows); pool.release(conn); // Never reached on early return} // ❌ Anti-Pattern 2: Release in wrong scopeasync function wrongScope(): Promise<void> { let conn: Connection | null = null; if (needsDatabase) { conn = await pool.acquire(); await conn.query('...'); } // conn might be null here - release may fail or be wrong pool.release(conn!); // Dangerous!} // ❌ Anti-Pattern 3: Async release without awaitasync function unawaited(): Promise<void> { const file = await open('data.txt', 'r'); try { return file.readFile('utf-8'); } finally { file.close(); // No await - close may not complete! }} // ❌ Anti-Pattern 4: Release-only-on-successasync function onlyOnSuccess(): Promise<void> { const conn = await pool.acquire(); try { await riskyOperation(conn); pool.release(conn); // Only on success! } catch (error) { // On error, connection never released! throw error; }} // ❌ Anti-Pattern 5: Storing resource for laterclass BadCache { private connection?: Connection; async query(sql: string): Promise<Result> { // Create connection lazily and store it if (!this.connection) { this.connection = await pool.acquire(); } return this.connection.query(sql); // When is this ever released? Never! }}If you acquire a resource, the very next line of code should be 'try {' and there should be a corresponding 'finally { release }' block. Don't do anything between acquire and try. This pattern is mechanical and fool-proof.
The resource lifecycle—acquisition, usage, release—is the foundation of all resource management. Let's consolidate what we've learned:
What's next:
Now that we understand the lifecycle, we'll explore why resource management matters—the real-world consequences of resource mismanagement, from memory leaks to production outages to financial costs.
You now have a deep understanding of the resource lifecycle and its phases. This knowledge enables you to design resource-handling code that correctly manages resources through their entire journey, from acquisition to release. Next, we'll see why this matters in real systems.