Skip to content
Code Card

Scope-based Resource Management

Resources that "must be closed once opened" — such as files, sockets, database connections, locks, and temporary directories — must be handled differently from ordinary values. The scope that acquires a resource is responsible for releasing it.

Practice memo · 24 min read · Hard


Scope-based Resource Management

Files, sockets, DB connections, locks, and temporary directories all fall into the category of "things that must be closed once opened" and must be treated differently from ordinary values. The scope that acquires a resource is responsible for releasing it.

Ordinary values can be left to the GC or the ownership model, but resources such as OS handles, connection pool slots, and file locks occupy external state. Delayed release causes failures more immediate than memory leaks.

The intent of this pattern is to align resource lifetimes with code scopes. It makes the cleanup responsibility visible near where the resource is acquired, and ensures that cleanup is never skipped even if a return, exception, or cancellation occurs in between. C#'s using disposes an IDisposable instance when the block exits, and guarantees disposal even if an exception is thrown inside the block.1 Python's with was introduced to abstract the try/finally pattern, and a context manager's __enter__/__exit__ are called on entry and exit.2

In TypeScript/JavaScript, try/finally is the baseline construct when targeting the broadest runtime compatibility. JavaScript's resource management differs from memory management: the runtime can reclaim memory automatically, but external resources such as file handles, network connections, and stream reader locks must be explicitly closed or released.3 The finally block executes before control leaves the try...catch...finally flow.4 Starting with TypeScript 5.2, however, using / await using declarations have been introduced (TC39 Explicit Resource Management, Stage 3), similar in intent to C#'s using. The error-combining semantics differ, though: on the JS/TS side, SuppressedError semantics are used to preserve both a dispose exception and a body exception together. When an object implements [Symbol.dispose] or [Symbol.asyncDispose], scope-based resource release is supported at the language level.56 Rust uses a model closer to RAII, where the Drop destructor runs when a value goes out of scope.7

Core Formula:

Text
Scope Resource = Acquisition and release points are co-located in the same scope
Cleanup Guarantee = Release is guaranteed on success, failure, and early return alike
Policy Boundary = Release is handled locally; failure interpretation is handled by the upper-level policy

1. The Problem

Suppose you are building a feature that appends audit log entries to a file. Events such as logins, permission changes, and payment status changes are recorded line by line in audit.log.

Opening and closing a file handle may look simple, but in production the following problems arise frequently.

  • Returning early when the message is empty without closing the file handle

  • If an exception occurs during append, close will not be executed

  • File handles accumulate, causing file locks to remain unreleased on Windows

  • close failure overwrites the original write failure

  • Mixing resource cleanup and business failure handling in a single function

The domain here is singular: safely open, write to, and close an audit log file.

The key is simple: open it, use it, then close it.

Personal note: Think of it like turning off the gas valve when you leave the house. You open the valve to use the gas, then there's a procedure to close it. Personally, I like the gas valve analogy because the first couple of times you leave the valve open, nothing bad may happen, but when an accident does occur, it's a major one — which is exactly the shared characteristic here.

Inherent limitations of this example (must read): The code below is meant to illustrate a resource lifecycle, not a high-volume logging architecture. Opening, writing, and closing a file for every single audit log entry will exhaust OS file handles and disk IOPS under thousands of payment or login events per second, causing system collapse. Real high-volume logging should buffer events in memory via a queue (such as Channel<T>), then have a background worker tied to the application lifetime keep the stream open and perform batch writes.[12] For detailed background worker lifetime management, follow the same principles described in the PER/fire-and-forget section of Cache-Aside.

2. Core Implementations

C#

C#
using System;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

public sealed class AuditLogWriter
{
    private readonly string filePath;

    public AuditLogWriter(string filePath)
    {
        this.filePath = filePath ?? throw new ArgumentNullException(nameof(filePath));
    }

    public string GetFilePath()
    {
        return this.filePath;
    }

    public async ValueTask WriteLineAsync(
        string message,
        CancellationToken cancellationToken)
    {
        if (string.IsNullOrWhiteSpace(message))
        {
            throw new ArgumentException("message cannot be empty.", nameof(message));
        }

        await using FileStream stream = new(
            this.filePath,
            FileMode.Append,
            FileAccess.Write,
            FileShare.Read,
            bufferSize: 16 * 1024,
            useAsync: true);
        await using StreamWriter writer = new(
            stream,
            Encoding.UTF8,
            bufferSize: 16 * 1024,
            leaveOpen: true);

        await writer.WriteLineAsync(message.AsMemory(), cancellationToken);
        await writer.FlushAsync(cancellationToken);
    }
}

FlushAsync is closer to draining the application/stream buffer and does not guarantee durability against power loss or kernel buffer concerns. For power-failure safety in financial or audit logs, apply a separate fsync/flush-to-disk policy (such as FileStream.Flush(flushToDisk: true)).8

TypeScript (most conservative approach: try/finally)

export final class is not valid. final is not a TypeScript keyword. Use export class as shown below. Prohibiting inheritance cannot be expressed directly in TypeScript syntax alone; if needed, work around it with lint rules, code review conventions, or factory design. Note that private constructor also prevents external instantiation, so it is closer to a singleton/static factory pattern than a general-purpose final class substitute.

TypeScript
import { mkdir, open } from "node:fs/promises";
import { dirname } from "node:path";

export class AuditLogWriter {
  readonly #filePath: string;

  public constructor(filePath: string) {
    if (filePath.trim().length === 0) {
      throw new RangeError("`filePath` cannot be empty.");
    }
    this.#filePath = filePath;
  }

  public getFilePath(): string {
    return this.#filePath;
  }

  public async writeLineAsync(
    message: string,
    signal?: AbortSignal,
  ): Promise<void> {
    if (message.trim().length === 0) {
      throw new RangeError("`message` cannot be empty.");
    }
    signal?.throwIfAborted();

    await mkdir(dirname(this.#filePath), { recursive: true });

    const handle = await open(this.#filePath, "a");
    try {
      signal?.throwIfAborted();
      // If you want not only a pre-start check but also cancellation during a write, pass a `signal` as an option.
      await handle.appendFile(`${message}\n`, { encoding: "utf8", signal });
    } finally {
      await handle.close();
    }
  }
}

TypeScript 5.2+ (using / await using)

Starting with TypeScript 5.2, scope-based resource management can be expressed at the language level. When a scope exits (whether by normal completion, early return, or exception), [Symbol.asyncDispose] is called automatically. Symbol.dispose/Symbol.asyncDispose is supported in Node 18.18+, Deno 1.37+, Safari 18.3+, and others; a symbol polyfill is required on unsupported runtimes.910 Notably, Node's FileHandle[Symbol.asyncDispose]() was added in v20.4.0 / v18.18.0 and became non-experimental starting in v24.2.0. For conservative production documentation, it is safer to target Node 24.2+ or fall back to explicit close()/try-finally.11

One important note is that Symbol.asyncDispose support and native parser support for the using syntax are separate layers. TypeScript can transpile the syntax, but the execution runtime must have Symbol.dispose/Symbol.asyncDispose (or a polyfill) for it to actually work. In other words, symbol availability, FileHandle's asyncDispose implementation, TS transpilation, and runtime native syntax support are each distinct requirements.

TypeScript
import { mkdir, open } from "node:fs/promises";
import { dirname } from "node:path";

export class AuditLogWriterModern {
  readonly #filePath: string;

  public constructor(filePath: string) {
    if (filePath.trim().length === 0) {
      throw new RangeError("`filePath` must not be empty.");
    }
    this.#filePath = filePath;
  }

  public async writeLineAsync(message: string): Promise<void> {
    if (message.trim().length === 0) {
      throw new RangeError("`message` must not be empty.");
    }
    await mkdir(dirname(this.#filePath), { recursive: true });

    // Node's `FileHandle` implements `[Symbol.asyncDispose]` starting from v20.4.0 / v18.18.0
    // (non-experimental as of v24.2.0). When the scope exits, the handle is closed automatically.
    // (The lifetime management intent is similar to C#'s `await using`.)
    await using handle = await open(this.#filePath, "a");
    await handle.appendFile(`${message}\n`, { encoding: "utf8" });
  }
}

Python

Python
from pathlib import Path

class AuditLogWriter:
    def __init__(self, file_path: str) -> None:
        if not file_path.strip():
            raise ValueError("`file_path` cannot be empty.")
        self._file_path = Path(file_path)

    def get_file_path(self) -> Path:
        return self._file_path

    def write_line(self, message: str) -> None:
        if not message.strip():
            raise ValueError("`message` cannot be empty.")
        self._file_path.parent.mkdir(parents=True, exist_ok=True)
        with self._file_path.open("a", encoding="utf-8") as file:
            file.write(f"{message}\n")

Rust

Rust
use std::fs::OpenOptions;
use std::io;
use std::io::Write;
use std::path::{Path, PathBuf};

pub struct AuditLogWriter {
    file_path: PathBuf,
}

impl AuditLogWriter {
    pub fn new(file_path: impl Into<PathBuf>) -> Self {
        Self {
            file_path: file_path.into(),
        }
    }

    pub fn get_file_path(&self) -> &Path {
        &self.file_path
    }

    pub fn write_line(&self, message: &str) -> io::Result<()> {
        if message.trim().is_empty() {
            return Err(io::Error::new(
                io::ErrorKind::InvalidInput,
                "`message` cannot be empty.",
            ));
        }
        if let Some(parent) = self.file_path.parent() {
            // If only a relative filename is provided, `parent` may be an empty string (`""`), so we filter that out explicitly.
            if !parent.as_os_str().is_empty() {
                std::fs::create_dir_all(parent)?;
            }
        }
        let mut file = OpenOptions::new()
            .create(true)
            .append(true)
            .open(&self.file_path)?;
        writeln!(file, "{message}")?;
        file.flush()?;
        Ok(())
    }
}

3. Call Site

The caller does not need to know about resource cleanup directly. The caller validates events, converts them to log lines, and delegates I/O failures to a higher-level policy. When to close the file handle is the responsibility of the inner scope inside AuditLogWriter.

TypeScript
type AuditEvent = Readonly<{
  actorId: string;
  action: "login" | "logout" | "permissionChanged";
  targetId: string;
  occurredAt: Date;
}>;

type AuditIoPolicy = Readonly<{
  runAsync: (operation: () => Promise<void>) => Promise<void>;
}>;

function toAuditLine(event: AuditEvent): string {
  if (event.actorId.trim().length === 0) {
    throw new RangeError("`actorId` cannot be empty.");
  }
  if (event.targetId.trim().length === 0) {
    throw new RangeError("`targetId` cannot be empty.");
  }
  return [
    event.occurredAt.toISOString(),
    event.actorId,
    event.action,
    event.targetId,
  ].join("\t");
}

export async function appendAuditEventAsync(
  event: AuditEvent,
  writer: AuditLogWriter,
  ioPolicy: AuditIoPolicy,
  signal?: AbortSignal,
): Promise<void> {
  const line = toAuditLine(event);
  await ioPolicy.runAsync(async () => {
    await writer.writeLineAsync(line, signal);
  });
}

Separation of Concerns:

Text
toAuditLine             = Validates domain events and converts them to strings
AuditLogWriter          = Opens, writes to, and closes the file
AuditIoPolicy           = Handles retry, alerting, and failure policy
appendAuditEventAsync   = Assembles the domain flow

4. Reading Order

Text
Is the input empty?
→ Does the file path directory exist?
→ Has the resource been acquired?
→ Is the disposal scope visible immediately after acquisition?
→ Does cleanup execute regardless of whether the write operation succeeds or fails?
→ Does I/O failure interpretation propagate up to higher-level policy?

When reading resource code, you should look at "what must be released" before "what it does" — just as you check the due date before borrowing a library book.

5. Boundaries and Misconceptions

Scope-based cleanup is not a failure-recovery pattern. Closing a file and recovering from a failed file write are different responsibilities. using, with, finally, and Drop are responsible for guaranteeing cleanup. Retry logic, fallback behavior, alerting, and user-facing error translation belong to higher-level policy handlers.

Predictable domain failures and system failures should also be separated. An empty actorId in an audit event is a domain input error. In contrast, a full disk, missing permissions, or a read-only file system are infrastructure failures. Collapsing both into the same false or null makes it hard to trace the root cause in production.

One important caveat is that the release function itself can fail. In JavaScript's finally, if await handle.close() fails, it can overwrite the original appendFile failure. C#'s Dispose/DisposeAsync (and await using) can also throw exceptions, causing the same kind of overwrite. For critical audit logs or financial logs, a centralized policy is needed that allows both the original failure and the close failure to be observed.

Ownership boundary: whoever creates it closes it (watch out with DI)

Whoever creates a resource decides who releases it. Resources that are newed directly inside a function are closed when that scope exits. However, resources injected by a DI (IoC) container as Scoped or Singleton must not be arbitrarily closed with using/await using inside business logic functions. The container owns the lifetime of those objects, so another service that later receives the same injected instance will get an already-disposed instance and trigger a cascading failure via ObjectDisposedException. The principle is: "the party that creates a resource also owns responsibility for releasing it." Do not close injected resources locally; delegate their lifetime to the container's registration (scope) configuration.

Personal note: Just as you don't take the plate home after ordering food at a restaurant, the entity that creates a resource is responsible for it.

Guarding Against Exception Suppression (Double Fault)

The principle is as follows: propagate the business (original) exception first, observe close failures via loggers and metrics without overwriting the original exception, and surface close failures only when the body succeeds.

TypeScript
type AuditLogger = Readonly<{ error: (msg: string, meta?: unknown) => void }>;

export async function writeLineSafely(
  filePath: string,
  message: string,
  logger: AuditLogger,
): Promise<void> {
  const handle = await open(filePath, "a");

  let bodyOk = false;
  try {
    await handle.appendFile(`${message}\n`, { encoding: "utf8" });
    bodyOk = true;
  } finally {
    try {
      await handle.close();
    } catch (closeError) {
      // Always observe close failures. (metric: audit_log.close.failure.count++)
      logger.error("audit close failed", { closeError });

      if (bodyOk) {
        // Since the body succeeded, surface the close failure.
        throw closeError;
      }
      // If the body already failed, the original exception takes priority.
      // Not throwing here is what prevents the original exception from being suppressed (double fault prevention).
    }
  }
}

In C# as well, for critical logs, don't rely solely on await using's automatic dispose; instead, use the same manual pattern that prioritizes the original exception and observes DisposeAsync failures via a logger or metrics.12

Note that the "overwriting" mentioned here should be understood as applying only to manual try/finally and the general caveats of C#'s using family. ECMAScript Explicit Resource Management's using/await using addresses this problem by adopting semantics that preserve both the exception thrown during dispose and the exception from the body together as a SuppressedError. In other words, using keeps both exceptions accessible rather than collapsing them into one.13

Production failure modes:

  • There is a return before close after open

  • Placing close only at the last line of the success path

  • Returning inside finally overwrites the original exception

  • Storing resource objects as fields for too long, causing blurred lifetime boundaries

  • Completely ignoring dispose/close failures

  • Mixing release responsibility and retry policy in the same function

  • Mistakenly assuming that parallel file write calls guarantee append ordering

append mode can reduce file offset contention, but it does not guarantee application-level audit event ordering, logical serialization of multiple writes, or atomicity at the batch level. Because promise-based FS operations run on a thread pool and concurrent modifications to the same file are not synchronized, an audit log where ordering matters must be serialized through a single-writer queue or Channel.11

6. Incorrect Examples

TypeScript
import { open } from "node:fs/promises";

async function appendAuditBadAsync(
  filePath: string,
  message: string,
): Promise<void> {
  const handle = await open(filePath, "a");
  if (message.trim().length === 0) {
    return;
  }
  await handle.appendFile(`${message}\n`, { encoding: "utf8" });
  await handle.close();
}

Why this is bad:

  • If message is empty, return without closing.

  • If appendFile fails, close will not be executed.

  • The file handle's lifetime spans the entire function, but the release guarantee depends on a single last line.

  • The caller has no way of knowing that this function can leak a file handle.

  • Testing only the success path won't reveal the problem.

The minimal fix is try/finally.

TypeScript
async function appendAuditBetterAsync(
  filePath: string,
  message: string,
): Promise<void> {
  if (message.trim().length === 0) {
    throw new RangeError("message cannot be empty.");
  }
  const handle = await open(filePath, "a");
  try {
    await handle.appendFile(`${message}\n`, { encoding: "utf8" });
  } finally {
    await handle.close();
  }
}

With TypeScript 5.2+, you can write it more concisely using await using.

7. Production Extensions

The core test for this pattern is not "closes on success" but closes even on failure. Rather than writing to the real filesystem every time, verify the release guarantee using a test double that lets you observe whether the resource was actually closed.

TypeScript
import assert from "node:assert/strict";
import test from "node:test";

type AsyncCloseable = Readonly<{
  closeAsync: () => Promise<void>;
}>;

class DisposableProbe implements AsyncCloseable {
  #closed = false;
  #closeCalls = 0;

  public getClosed(): boolean {
    return this.#closed;
  }

  public getCloseCalls(): number {
    return this.#closeCalls;
  }

  public async closeAsync(): Promise<void> {
    this.#closed = true;
    this.#closeCalls += 1;
  }
}

async function usingResourceAsync<TResource extends AsyncCloseable>(
  resource: TResource,
  bodyAsync: (resource: TResource) => Promise<void>,
): Promise<void> {
  try {
    await bodyAsync(resource);
  } finally {
    await resource.closeAsync();
  }
}

test("Close resources on the success path", async () => {
  const probe = new DisposableProbe();
  await usingResourceAsync(probe, async () => {
    return;
  });
  assert.equal(probe.getClosed(), true);
  assert.equal(probe.getCloseCalls(), 1);
});

test("Close resources on the failure path", async () => {
  const probe = new DisposableProbe();
  await assert.rejects(async () => {
    await usingResourceAsync(probe, async () => {
      throw new Error("Write failure");
    });
  });
  assert.equal(probe.getClosed(), true);
  assert.equal(probe.getCloseCalls(), 1);
});

This test validates the lifetime policy, not the correctness of file writes. In production, you would attach metrics here.

Text
audit_log.write.success.count
audit_log.write.failure.count
audit_log.close.failure.count
audit_log.write.duration.ms

The reason for separating metric names is that write failures and close failures represent different kinds of faults. A write failure may stem from disk, permission, or path issues, while a close failure may stem from flush delays or file system state problems.

Personal note: If you read The Little Prince, there's a line: "Grown-ups love figures." Running a program in production is, in the end, a grown-up's job. Having audit_log.write.failure.count and audit_log.close.failure.count increment separately is far better than saying "the log seems to not be writing sometimes." In that sense, I think the saying "grown-ups love figures" has the cause and effect reversed. Once you become a grown-up, you have no choice but to love figures — if you don't want your contracts to fall apart.

High-volume logs should not open/close on every line

As warned earlier, the "open → write → close per line" structure in this example is meant to illustrate the lifecycle, not to serve as a throughput design. Under thousands of requests per second, file handle creation/destruction and disk IOPS become bottlenecks that bring the system down. In practice, the approach is roughly as follows.

Text
Event → Channel<T> / Queue (in-memory buffer)
      → Background worker tied to application lifetime (BackgroundService)
      → Batch Write with stream kept open + periodic Flush
      → On shutdown, gracefully flush remaining buffer and close

In other words, scope-based resource management is a tool for handling the lifetime of "a single unit of work," while high-throughput scenarios are handled with long-lived resources tied to "application lifetime" combined with batch writes. The key is to distinguish between these two lifetime axes. Managing the lifetime and cancellation of background workers follows the same principle as using IHostedService/Channel<T> instead of fire-and-forget in Cache-Aside.14

Personal note: In practice, it's common to see code that opens, writes to, and closes a file all within a single method. Usually this happens when someone copies code from a blog or example, and ships it as-is because "it works." I write that kind of code too.

If traffic is low, call frequency is minimal, and the blast radius of a failure is limited, a simple open-write-close structure is fine. As long as nothing goes wrong, there's no need to overcomplicate it.

Graceful Shutdown sequence

When using long-lived workers, you must flush any events remaining in the queue on shutdown (SIGTERM) without losing them. In C# IHostedService/BackgroundService, ordering is critical.

Text
1. Enter `StopAsync(hostCancellationToken)` (the host sends a shutdown signal)
2. Stop accepting new events: call `channel.Writer.Complete()` to reject further writes
3. Drain the remaining queue: use `await foreach` to process all remaining items from the Reader
   - If you pass `hostCancellationToken` directly here, it will be cancelled immediately and logs will be lost.
     Wait for completion using a separate token that provides a dedicated grace period for draining.
4. Final flush: call `FlushAsync` on the stream (plus flush-to-disk if needed)
5. Close the stream/handle

The key point is to split the cancellation token into two layers: "stop accepting new work" happens immediately, and "drain remaining work" completes within a grace period (e.g., HostOptions.ShutdownTimeout). The close must come last, only after the drain and the final flush are both done. If the order is reversed and the stream is closed first, writes still in progress during the drain will hit an already-closed handle, causing data loss and exceptions. Here too, "the order of open and close" is what guarantees consistency. Consistency does not need to be thought of as something complicated. It simply means checking that the resource's state transitions happen in the correct order: Did you avoid writing before opening? if you opened, did you close? did you write again after closing? and did you finish all remaining work before closing? Did you properly carry out each step in the right order? That is what consistency means.

8. C# / TypeScript / Python / Rust Comparison Notes

Language

Idioms

What Not to Force Into the Language

C#

using, await using, IDisposable, IAsyncDisposable

The habit of handling every failure locally with try/catch

TypeScript

try/finally, using/await using (5.2+), AbortSignal, explicit close()

Mistaking Rust-style RAII for something the runtime always guarantees

Python

with, context manager, contextlib

File handle held as a field for an extended object lifetime, Java-style

Rust

Ownership, scopes, Drop, Result

Assuming "it'll get closed eventually" like in GC languages; blindly trusting that synchronous Drop will fully release async I/O (explicit shutdown().await is required)

C# exposes lifetime contracts for resource types through IDisposable/IAsyncDisposable. TypeScript supports a variety of standard runtime environments, so the most conservative approach is try/finally; in version 5.2+ with supporting runtimes, using/await using expresses the same lifetime management intent more declaratively. Python's with statement is the most readable option, and the contextlib family is well-suited for dynamically stacking multiple resources.15 In Rust, a value's ownership and scope are directly tied to its resource lifetime.

In Rust specifically, async cleanup requires careful attention. Drop is a synchronous function and cannot execute async fn (AsyncDrop remains an unsolved problem in the ecosystem). When closing files or sockets under a runtime like Tokio, even if a value goes out of scope and Drop is called, you cannot safely await an async flush or a connection-termination packet. Therefore, for shutdowns where ordering matters, you should explicitly call shutdown().await for sockets and general AsyncWrite implementations, call flush().await for files (or sync_all().await if durability is required), or hand off the cleanup work to a background task. In practice, Tokio's File may not close immediately on drop if there is pending I/O, so the documentation advises calling flush before dropping.16 A synchronous Drop is a safety net that ensures nothing is forgotten, not a mechanism that waits for async operations to complete.

Express the same intent without forcing syntactic uniformity. What matters is the meaning: the code that acquires a resource is also responsible for releasing it.

9. Further Considerations

  • Should this resource be confined to a function-level scope, or is it acceptable for it to live longer as an object field?

  • Should a close/dispose failure overwrite the original operation failure, or should both be recorded?

  • Does the order of audit log appends matter, and if so, where will parallel calls be serialized?

  • Is this failure a domain input error or a file system infrastructure error?

  • Open and close the file each time, or keep the writer alive for a long time and batch writes?

  • Does the test verify cleanup not only on the success path but also on exception and early-return paths?

It sounds complicated when framed that way, but it essentially comes down to how you manage the lifetime of a resource. What matters is the flow of resources. Once you have that flow as your skeleton, you layer on top of it the flesh: error handling, retries, flush, and so on.

10. Summary

Files, sockets, DB connections, and locks are not ordinary values; they require explicit lifetime management.

Scope-based resource management is a pattern for guaranteeing cleanup on success, failure, and early return alike. C# expresses this intent with using, Python with with, TypeScript with try/finally (or using in 5.2+), and Rust with Drop. It is safer to handle release in the local scope and delegate failure interpretation and retry policy to a higher-level policy handler. Production tests must verify not just "closes on success" but also "closes on failure." For high-volume logs, avoid opening and closing a file on every line as shown in this example; instead, use a queue, a long-lived background worker, and batch writes.

Easy way to remember

The scope that acquires a resource must release it.

Footnotes

  1. Microsoft Learn. using statement — ensure the correct use of disposable objects (C#)
  2. Python. PEP 343 – The "with" Statement
  3. MDN Web Docs. JavaScript resource management guide
  4. MDN Web Docs. try...catch (finally)
  5. TypeScript. TypeScript 5.2 release notes — using declarations and Explicit Resource Management
  6. TC39. Explicit Resource Management proposal (Stage 3)
  7. Rust Documentation. Drop in std::ops
  8. Microsoft Learn. FileStream.FlushAsync (flush stream buffer; disk durability via Flush(flushToDisk))
  9. MDN Web Docs. Symbol.dispose
  10. MDN Web Docs. Symbol.asyncDispose
  11. Node.js. [File system: filehandle.appendFile(signal option), FileHandle[Symbol.asyncDispose](), caution regarding concurrent modifications](https://nodejs.org/api/fs.html)
  12. Microsoft Learn. IAsyncDisposable / DisposeAsync
  13. V8. JavaScript's New Superpower: Explicit Resource Management
  14. Microsoft Learn. System.Threading.Channels (Channel integration with BackgroundService)
  15. Python. contextlib — Utilities for with-statement contexts
  16. Tokio. tokio::fs::File (flush before drop recommended if I/O is incomplete; use sync_all for durability guarantee)