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!'));
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();
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(),
]);
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;
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;
}
// ...
};
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);
// ...
};
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!
Top comments (1)
I didn't know about much of this! Thanks for sharing; I actually think this will be very useful for me!