Using async functions for Promise wrapping
Beyond allowing the await keyword to be used within them, JavaScript’s async functions have their own useful aspects. As an alternative to Promise.resolve() and Promise.reject(), consider using async functions to cleanly wrap return values in Promises for convenient mocks in your test suite and to ensure consistent return types.
Summary
Regardless of whether we use await
or Promise chains for control flow, marking functions with async
when they return Promises provides meaningful benefits in terms of reducing boilerplate and ensuring all code paths return a Promise.
Wrapping with Promise’s static resolve and reject methods
Promise.resolve()
and Promise.reject()
are simple utilities to wrap synchronous output of functions that need return a Promise. These static methods have two main use cases: mocking return values and returning consistent values when not all code paths call async functions. As long as we remember to wrap any non-Promise return value with a Promise.resolve()
, we have a clean and concise way to return Promises wrapping non-asynchronous code paths to expose a consistent output interface from our functions.
const getMockDataAsync = shouldSucceed => {
if (!shouldSucceed) {
return Promise.reject(new Error('An error occurred.'));
}
return Promise.resolve({ foo: 'bar' });
};
const doActionIfNecessary = data => {
if (data.shouldDoAction) {
// doActionAsync() returns a Promise, so doActionIfNecessary() can return this to its own caller.
return doActionAsync(data);
}
// doActionIfNecessary() should always returns a Promise; in this case, return a fulfilled one.
return Promise.resolve();
};
Wrapping return values in Promises using async functions
The async
keyword has an interesting behavior of wrapping a function’s output such that it always returns a Promise. Refactoring the above examples, this can be taken advantage of to the effect that the static Promise methods are no longer necessary — returning a value or even an implicit return of undefined
will result in returning a fulfilled Promise. Similarly, throwing will result in returning a rejected Promise. This wrapping behavior isn’t just useful for brevity — it also ensures we don’t forget to wrap the output of one code path with a Promise.resolve()
.
const getMockDataAsync = async shouldSucceed => {
if (!shouldSucceed) {
throw new Error('An error occurred.');
}
return { foo: 'bar' };
};
const doActionIfNecessary = async data => {
if (data.shouldDoAction) {
return doActionAsync(data);
}
// Here, we no longer perform an empty Promise.resolve(); the implicit return is itself a fulfilled
// Promise with the value of undefined.
};
Let’s take a second look at these async function examples with explicit TypeScript annotations for the return type. With these, I find it’s immediately clear what the return values are.
const getMockDataAsync = async (shouldSucceed: boolean): Promise<IData> => {
if (!shouldSucceed) {
throw new Error('An error occurred.');
}
return { foo: 'bar' };
};
const doActionIfNecessary = async (data: IActionData): Promise<void> => {
if (data.shouldDoAction) {
return doActionAsync(data);
}
};