Appearance
Automatic Resource Management
When to use
Use this pattern when you need guaranteed cleanup of a resource — temporary directories, database connections, file handles, server instances — even when errors occur. The pattern prevents resource leaks by coupling resource creation and disposal into a single callback-scoped API.
The pattern
A factory function defineManagedResource takes a create function and a dispose function, then returns an async "using" function. The caller passes a callback that receives the resource. Disposal runs in a finally block, so it executes whether the callback succeeds or throws.
Using a managed resource
The usingTempDir function takes an async callback that receives a temporary directory path. The directory exists for the duration of the callback and is automatically deleted afterward — even if the callback throws:
javascript
const result = await usingTempDir(async (dir) => {
// dir exists here
await writeFile(join(dir, "data.json"), payload);
return await processDir(dir);
});
// dir has been deleted, even if processDir threwDefining a managed resource
Call defineManagedResource with a create function and a dispose function. It returns an async function that handles the create → use → dispose lifecycle:
javascript
import { rm } from "node:fs/promises";
import { temporaryDirectory } from "./temp.js";
import { defineManagedResource } from "./using.js";
export const usingTempDir = defineManagedResource(
async () => temporaryDirectory(),
async (tempDir) => {
await rm(tempDir, { recursive: true });
},
);Test isolation
Each test gets its own temporary directory that is automatically cleaned up:
javascript
import { describe, it, expect } from "vitest";
import { usingTempDir } from "./temp.js";
describe("glob", () => {
it("finds matching files", async () => {
await usingTempDir(async (dir) => {
await writeFile(join(dir, "a.txt"), "");
await writeFile(join(dir, "b.js"), "");
const matches = await glob("*.txt", { cwd: dir });
expect(matches).toEqual(["a.txt"]);
});
});
});Lifecycle ordering
The create → use → dispose sequence is guaranteed:
javascript
const log = [];
const usingResource = defineManagedResource(
async () => {
log.push("create");
return { id: 1 };
},
async () => {
log.push("dispose");
},
);
await usingResource(async (resource) => {
log.push("use");
});
console.log(log);
// => ["create", "use", "dispose"]Error handling
When the callback throws, disposal still runs and the original error propagates:
javascript
try {
await usingTempDir(async (dir) => {
throw new Error("something failed");
});
} catch (error) {
// error.message === "something failed"
// tempDir has already been cleaned up
}Implementing defineManagedResource()
The factory returns an async function that creates the resource, runs the caller's callback inside a try block, and disposes the resource in a finally block — guaranteeing cleanup whether the callback succeeds or throws.
javascript
export function defineManagedResource(
createResource,
disposeResource,
) {
return async function usingManagedResource(useFn) {
const resource = await createResource();
try {
return await useFn(resource);
} finally {
await disposeResource(resource);
}
};
}Trade-offs
- Pros: Guaranteed cleanup via
finally, composable (define once, reuse everywhere), return values pass through, works with any async resource. - Cons: Callback nesting — each managed resource adds one level of indentation. Not as ergonomic as the TC39 Explicit Resource Management proposal (
usingkeyword withSymbol.dispose). - Versus TC39
using: ThedefineManagedResourcepattern works in all current runtimes today. Whenusingdeclarations andSymbol.disposereach broad support, consider migrating. The callback pattern remains useful for cases where the dispose function needs async cleanup (Symbol.asyncDispose).