What is the async disposer pattern in Javascript

How to automatically close and cleanup resources

Author's image
Tamás Sallai
5 mins

Try-with-resources

A recurring pattern is to run some initialization code to set up some resource or configuration, then use the thing, and finally do some cleanup. It can be a global property, such as freezing the time with timekeeper, starting a Chrome browser with Puppeteer, or creating a temp directory. In all these cases, you need to make sure the modifications/resources are properly disposed of, otherwise, they might spill out to other parts of the codebase.

For example, this code creates a temp directory in the system tmpdir then when it's not needed it deletes it. This can be useful when, for example, you want to use ffmpeg to extract some frames from a video and need a directory to tell the ffmpeg command to output the images to.

// create the temp directory
const dir = await fs.mkdtemp(await fs.realpath(os.tmpdir()) + path.sep);
try {

	// use the temp directory

} finally {
	// remove the directory
	fs.rmdir(dir, {recursive: true});
}

A similar construct is console.time that needs a console.timeEnd to output the time it took for a segment of the code to run:

console.time("name");
try {
	// ...
} finally {
	console.timeEnd("name");
}

Or when you launch a browser, you want to make sure it's closed when it's not needed:

const browser = await puppeteer.launch({/* ... */});
try {
	// use browser
} finally {
	await browser.close();
}

All these cases share the try..finally structure. Without it, an error can jump over the cleanup logic, leaving the resource initialized (or the console timing still ticking):

const browser = await puppeteer.launch({/* ... */});

// if there is an error here the browser won't close!

await browser.close();

In other languages, such as Java, this is known as try-with-resources and it is built into the language. For example, the BufferedReader is closed after the block:

try (BufferedReader br = new BufferedReader(new FileReader(path))) {
	return br.readLine();
}

This builds on the AutoCloseable interface's close method so it's easy to adapt to custom resource types.

But there is no such structure in Javascript. Let's see how to implement one!

Disposer pattern

One problem with the try..finally structure we've seen above is how to return a value from inside the try block. Let's say you want to take a screenshot of a website and want to use the resulting image later.

const browser = await puppeteer.launch({/* ... */});
try {
	const page = await browser.newPage();
	// ...
	const screenshot = await page.screenshot({/* ... */});

	// the browser can be closed but how to use the screenshot outside the try..finally?
} finally {
	await browser.close();
}

A suboptimal solution is to declare a variable outside the block and use it to store the image:

let screenshot;

const browser = await puppeteer.launch({/* ... */});
try {
	const page = await browser.newPage();
	// ...
	screenshot = await page.screenshot({/* ... */});
} finally {
	await browser.close();
}

// use screenshot

A better solution is to use a function:

const makeScreenshot = async () => {
	const browser = await puppeteer.launch({/* ... */});
	try {
		const page = await browser.newPage();
		// ...
		return await page.screenshot({/* ... */});
	} finally {
		await browser.close();
	}
}

const screenshot = await makeScreenshot();

This is the basis of the disposer pattern. The difference is that instead of hardcoding the logic inside the try..finally block, it gets a function that implements that part:

const withBrowser = async (fn) => {
	const browser = await puppeteer.launch({/* ... */});
	try {
		return await fn(browser);
	} finally {
		await browser.close();
	}
}

const screenshot = await withBrowser(async (browser) => {
	const page = await browser.newPage();
	// ...
	return await page.screenshot({/* ... */});
});

The withBrowser function contains the logic to launch and close the browser, and the fn function gets and uses the browser instance. Whenever the argument function returns, the browser is automatically closed, no additional cleanup logic is needed. This structure provides an elegant way to prevent non-closed resources hanging around.

An interesting aspect of this pattern is that it is one of the few cases where there is a difference between return fn() and return await fn(). Usually, it does not matter if an async function returns a Promise or the result of the Promise. But in this case, without the await the finally block runs before the fn() call is finished.

A potential problem is when there is some task is running in the argument function when it returns. This can happen when it starts an asynchronous task and does not wait for it to finish. In this case, the cleanup logic runs and closes the browser instance that can cause an error. It is usually the sign that an await is missing.

Conclusion

The async disposer pattern is a useful abstraction when there is a resource that needs cleaning up after usage. Using a function that handles the lifecycle of the resource and separates it from the logic that uses the resource makes sure that the cleanup is run.

December 11, 2020

Free PDF guide

Sign up to our newsletter and download the "How Cognito User Pools work" guide.


In this article