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

How to write a simple, generic Result type to hold a value or Error.

2018
TypeScript

How to integrate build-time or server-side syntax highlighting for markdown code fences with two libraries: markdown-it and Highlights (Atom’s syntax highlighting engine).

2017
Node.js
JavaScript
Performance

A Firefox issue where right click on a button results in a click event listener firing because of event delegation performed automatically by a JavaScript SPA framework.

2017
Firefox
JavaScript