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);
  }
};