Hollow Promises: Async JavaScript

oculus42

Samuel Rouse

Posted on February 21, 2024

Hollow Promises: Async JavaScript

Did you know Promise.all() can resolve synchronously?! Here are some quick thoughts on ways to use promises and async/await effectively and efficiently.

Missing Promises

If we aren't sure if something is a promise, we can wrap it in Promise.resolve(). Non-promise values – including undefined – are wrapped in a resolved promise, while promises will be passed through unchanged.

// Any non-promise value is resolved
Promise.resolve().then(() => console.log('This works!'));
Promise.resolve([1,2,3]).then(() => console.log('This, too'));

// Promises pass through
const wait1 = new Promise((res) => setTimeout(res, 1000));
Promise.resolve(wait1).then(() => console.log('One second'));

// Rejections pass through
const bad1 = new Promise((res, rej) => setTimeout(rej, 2000));
Promise.resolve(bad1).catch(() => console.log('Caught!'));
Enter fullscreen mode Exit fullscreen mode

If you need to accept a value or a promise for a value, using Promise.resolve() smoothes out the differences between sync and async.

Future-Proof

If a function has the possibility to return either, wrapping it in Promise.resolve() – or prefixing await if you prefer async/await – ensures that it works.

Extreme Edge Cases

Note that either of these options will introduce an asynchronous delay in processing synchronous values. Most times this is not a concern, but inside code that is executed frequently, like a web server handling thousands of requests per second, that delay could be a consideration.

Synchronous Promises

There is a scenario in which Promise.all() will resolve synchronously. It's probably not relevant but Promise.all([]) will execute synchronously. If you have logic that collects multiple sources and can return no promises to execute, this scenario can happen to you.

MDN has more detail on this phenomenon.

Not so Async

Using async/await has the ability to make asynchronous code read and operate almost like synchronous code, but that can cause problems when you need to make multiple requests. Using the normal format it is easy to cause your requests to be serialized, or to run one after the other, and slow your project down.

const userData = await getUserData();
const appData = await getSettings();
Enter fullscreen mode Exit fullscreen mode

In this example the getSettings() call will not start until the getUserData() call has returned, even though there is no direct dependency. There are a couple of patterns for working around that.

Promise.all

When async fails, you can fall back to Promises.

const [userData, appData] = await Promise.all([
  getUserData(),
  getSettings(),
]);
Enter fullscreen mode Exit fullscreen mode

Promise.all() allows multiple requests to start before we reach the await keyword, and we can easily restructure the array of results for consumption.

Await Later

It's common to put await at the earliest point it can be, but you can call the promises/async functions and await them later.

// Make both requests before calling await
const userPromise = getUserData();
const appPromise = getSettings();

const userData = await userPromise;
const appData = await appPromise;
Enter fullscreen mode Exit fullscreen mode

This lets both requests start before the code "pauses" waiting for the results.

Mix and Match

Sometimes adding a little Promise logic into mostly async/await code can save a lot of trouble.

Let's imagine you have a call to a service that might not return a result for some users, or perhaps the service is unreliable. Either way, we may have a prepared "fallback" result if the call fails. With async/await, we're probably using try/catch to handle this.

// Server is powered by a solar panel
const callUnreliableService = async () => {
  let result;
  try {
    result = await callService();
  } catch {
    // Maybe it was dark
    result = defaultResult;
  }
  // ...
};
Enter fullscreen mode Exit fullscreen mode

While async/await makes code more like synchronous code, synchronous error handling has always felt clunky to me with the nested scopes and the execution order nuances of catch and finally. Promises have "free" error handling in the form of .catch(), which we can use to simplify many error cases.

// Server is powered by a solar panel
// If it is dark, give us a default response.
const callUnreliableService = async () => {
  const result = await callService.catch(() => defaultResult);
  // ...
};
Enter fullscreen mode Exit fullscreen mode

With an inline .catch() you can provide a static default or trigger a backup call. This can fail as well, but using small, inline promise chains can help keep a function and its error handling closely linked.

Conclusion

Whether you are dealing with network requests, timers, or event handling, asynchronous code is critical to most modern applications. There are many styles to choose from when writing your code, and knowing the strengths and weaknesses of different patterns can make your code more reliable and easier to maintain.

Do you have some asynchronous thoughts to share? We await them patiently, with no catch!

💖 💪 🙅 🚩
oculus42
Samuel Rouse

Posted on February 21, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related