Writing async safe code: walkthrough
I recently started a new side project create-app, to generate an entire web app from data models.
And I ran into a familiar problem and wanted to share my solution.
Problem
Even though JavaScript is single threaded it is still possible to have race conditions. Two bits of async code can compete for the same resources.
This is precisely the problem I have.
EEXIST: file already exists, mkdir 'video-game-app/src/api'
and this error is happening here
if (!(await dirExists(dest))) {
await fs.mkdir(dest);
}
it checks to see if the directory already exists, if not it creates it. But our code is asynchronous so we end up trying to make a directory that already exists.
If you are familiar with threads this likely feels familiar.
Options
There are a few ways to solve this type of problem
- We could make the code less async (where is the fun in this?)
- We could make sure our code executes all together (not really an option here, but if dirExists were sync we wouldn’t have an issue.)
- We could use traditional methods like mutex/locks
- We can cache the promise (this is what I am going to do, I love this option)
Solution
async function mkdirAsyncSafeHelper(dir) {
if (!(await dirExists(dir))) {
return await fs.mkdir(dir);
}
}const dirMap = {};
export async function mkdirAsyncSafe(dir) {
if (dirMap[dir]) {
return dirMap[dir];
} const promise = mkdirAsyncSafeHelper(dir);
dirMap[dir] = promise;
return promise;
}
Why this works
This works by taking advantage of the core way that async functions work. Under the hood an async functions immediately returns a promise in the same execution, meaning mkdirAsyncSafeHelper returns a promise long before the file operations occur.
And we can cache this promise.
if (dirMap[dir]) {
return dirMap[dir];
}
checks to see if our async job is already created and if so it returns the existing promise.
This is what I love about this solution both places that are trying to make the same directory will wait on the same promise. In both cases the directory will exist before anything is added to the directory.
It is important that inside of mkdirAsyncSafeHelper we bundle our two async tasks. The promise that mkdirAsyncSafeHelper returns will resolve after we check to see if the directory exists and the creation of the directory.
This solution also takes advantage of the fact that js modules are singletons and stores the state in dirMap which remains in scope for the entire life of the module.
Additional take aways
Caching promises is a nice way to make code async safe. This same approach can be used on api calls. Identical api calls can return a single promise.