Hollow Promises: Async JavaScript
Tags: javascript, webdev
Published: 2024-02-21 19:03:48

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()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/resolve). Non-promise values – including `undefined` – are wrapped in a resolved promise, while promises will be passed through unchanged.

```javascript
// 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!'));
```

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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all#asynchronicity_or_synchronicity_of_promise.all) 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.

```javascript
const userData = await getUserData();
const appData = await getSettings();
```

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.

```javascript
const [userData, appData] = await Promise.all([
  getUserData(),
  getSettings(),
]);
```

`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.

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

const userData = await userPromise;
const appData = await appPromise;
```

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. 

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

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.

```javascript
// 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);
  // ...
};
```

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!