Resources
What | Type | Link |
---|---|---|
MDN Resources | Reference | - Async - Promises |
Scotch IO Tutorial on Async/Await | Cheatsheet | Link |
Zellwk blog post | Short Overview | Link |
Google Developers Fundamentals Primer | Reference | Link |
Code With Hugo | Slides & Post | Link |
Promises
Have three states:
- Pending
- Fulfilled
- Rejected
Constructing:
- Preferred way is with explicit constructor:
return new Promise((resolve, reject){ //... hasError ? reject(error) : resolve(value); });
- Arrow Syntax:
- In callback
const jsonPromises = urls.map(async url => { const response = await fetch(url); return response.json(); });
- Simple
const myAsync = async () => { // }
- See SO
- In callback
Special Methods:
Singular
Promise.reject(reason)
- Returns new promise that is rejected with reason argument
Promise.resolve(value)
- Return new promise that is resolved with value argument
- Special: If the value passed to it is a promise itself, this will automatically "follow" that promise chain and wait to pass back the final resolved value.
- Good to use if you are unsure if a value is a promise or not
Array
Promise.allSettled(promiseArr)
- Pass an array of promises, and this will return a new single promise that only resolves when all the passed promises have completed.
- It passes back an array of objects that describe the results of each
- Pass an array of promises, and this will return a new single promise that only resolves when all the passed promises have completed.
Promise.all(promiseArr)
- Same as
Promise.allSettled()
, but rejects with value passed by first promise that rejects, if any do. - This is a good one to remember to use if you have a bunch of promises that you need to wait to finish, but they can be processed in parallel. E.g. use
await Promise.all(promiseArr)
- This is important, because
await
is blocking by nature.
- This is important, because
- Same as
Promise.race(promiseArr)
- Kind of the inverse of
Promise.all()
- returns a promise that fulfills or rejects with value of first promise that fulfills or rejects - e.g. the "race winner"
- Kind of the inverse of
The array methods above, especially
Promise.all
are also great for "batching" intensive operations to avoid overloading the stack and/or rate-limited resources! Just split a master queue of promises into size-limited arrays, and execute in batches.
Using
- The old way, or if you need to use non-async functions, is to use
.then()
,.catch()
, and,.finally()
.catch()
andthen()
both pass values with them, whereasfinally()
does not.- Only use
finally()
if you just need to wait until the promise is resolved, but don't care about success or how it was resolved.
- Only use
- Pass a function as the argument to all three, and then have your code inside the function (callbacks)
- Common to see arrow syntax here to keep things clean
- Does order of the three matter?
- Kinda - https://stackoverflow.com/a/42028776/11447682
- Usually better to have
.then()
->.catch()
- Usually better to have
- Kinda - https://stackoverflow.com/a/42028776/11447682
- Newer method - await the value of the promise inside an async function - the async/await pattern.
With Array map()
Using Promise.all()
with an array of promises is a really neat tool, but what if you don't have an array of promises, but rather, an array of things that need to be handled within an async
wrapped function?
Here is an example:
function randDelay(maxSec) {
const randInt = Math.floor(Math.random() * maxSec);
return new Promise(res => setTimeout(res, randInt * 1000));
}
function delay(sec) {
return new Promise(res => setTimeout(res, sec * 1000));
}
const nonPromiseArr = [20, 19, 21];
// How do we process nonPromiseArr with delay() and await all?
First, a correct answer, and usually the easiest way:
async function main() {
await Promise.all(nonPromiseArr.map(async maxDelay => {
await delay(maxDelay);
console.log(`Rand delay, with max of ${maxDelay} reached.`);
}));
console.log('All done!');
}
main();
There are lots of other working ways to write the above (for example, you could return an async IIFE), but the above is usually the "cleanest" way.
Promise Array Footguns
Now, for some incorrect examples:
First, a simple syntax misunderstanding:
/**
* This looks valid, but is not!!!
*/
async function main() {
await Promise.all(nonPromiseArr.map(maxDelay => async () => {
await delay(maxDelay);
console.log(`Rand delay, with max of ${maxDelay} reached.`);
}));
console.log('All done!');
}
main();
// Output:
// > All done!
So why did this not work, and our async functions not run? Well, although the return type of an async function is indeed a Promise, we returned the function itself, not the result (i.e., we did not invoke it)!
Second, if you need to make sure that all the async actions within your map execute before moving on, dont forget to await Promise.all()
the result of an array map that returns async functions.
For example, this is a common mistake:
async function main() {
nonPromiseArr.map(async delaySec => {
console.log(`Starting wait of ${delaySec}`);
await delay(delaySec);
console.log(`${delaySec} reached.`);
});
console.log('All done!');
}
main();
In the above, the inner part (within the async anonymous function) executes in the correct order - the "reached" message appears after the correct delay. However, since we forget to await the async functions themselves, the JS moves on immediately, so we see "All done!" before the inner functions have actually completed!
Async / Await
Await
- Pretty simple - the
await
keyword tells JS you want to pause execution of the current code block until the value is returned by a promise.- WARNING: This makes await blocking in nature. See notes in Promises section on recommended use of
Promise.all(promiseArr)
when applicable.
- WARNING: This makes await blocking in nature. See notes in Promises section on recommended use of
- WARNING: Await can only be used inside async functions.
Sample:
function queryApiQuota(){
return new Promise((resolve,reject)=>{
// Use settimeout to mock HTTP request delay
setTimeout(() => {
let randUsage = Math.floor(Math.random() * Math.floor(500));
resolve({
'msg' : 'Used ' + randUsage + '/500 quota.'
});
}, 2000);
});
}
async function getApiQuota(){
let quotaResult = await queryApiQuota();
console.log(quotaResult);
}
Async Functions - special notes
- They always return a promise, even if you explicitly return a value.
- Error handling is actually a little more complicated than just using promises
- Option A: Try/Catch blocks inside async function itself
- Lines that use await should be wrapped in a try/catch block. You can combine multiple await calls together in a single try block, and then the catch block should catch whatever one failed first
- Can get a little messy if you have tons of awaits in a single async function
- Option B: Check for error when calling the async function
- Since async functions automatically return a promise, if anything goes wrong inside it, including an await failing, the entire promise will reject
- This means you can do:
And it would effectively "capture" all errors that fall through from inside the async function, including any
myAsyncFunction().catch((error)=>{ // Handle error });
await
s that failed.
- This means you can do:
- Since async functions automatically return a promise, if anything goes wrong inside it, including an await failing, the entire promise will reject
- Option A: Try/Catch blocks inside async function itself
Warnings
Concept: Promises are really just signals
Something that took me a little too long to realize was that, like many, I had a conceptually incorrect understanding of sync-vs-async and promises, especially in terms of NodeJS. My initial understanding was something along the lines of "This stuff is magical! I can just put my code in promises to stop blocking execution and get the immediate benefits of the event loop!".
The truth is that Promises and Async/Await are really all about signals. Instead of sync code, where execution is blocked line by line, with Promises, subsections of code can be started and then, assuming the subsection acts async and doesn't block IO / main thread, the event loop checks back in / listens for the subsection to emit a signal that it has completed / emitted results.
This is similar to callbacks, but the signal-based syntax (
await
,then
, etc) is much easier to write and grok
This has extra importance because of the fact that JS (most of the time) is running single-threaded, so the event loop allows for concurrency (which requires signals), but not parallelism. (1).
This has several large implications and explains a lot about the nuances of Promises and Async/Await stuff, as well as the general JS ecosystem:
- Putting something in a promise or async function does not automatically make it async in nature!!!
- As soon as you put in blocking code (e.g. expensive operations, loops that don't exit fast, etc.), you are actually back to holding up the thread and other promises
- Popping up in GH issues: https://github.com/sindresorhus/ora/issues/86
- Example demo: https://repl.it/@joshuatz/Blocking-Promises-Example
- Promises start executing (resolving) as soon as they are created
- You might think that if you create a bunch of Promises with
new Promise()
and fill an array with them, that they would run concurrently as soon as you executePromise.all(promiseArr)
. This is incorrect; they actually start as soon as you construct them.- There are some easy ways around this, if you want to defer, such as wrapping in another function that generates the promise and returns it
- S/O - creating a promise without starting to resolve it
- If you have blocking code in a promise, this can mean that it can start blocking before you have even called
Promise.all
- You might think that if you create a bunch of Promises with
- This is why you should always have async in mind when writing JS, especially if it is going to be used by many other devs (for example in a library or module).
- You need to design things to be async from the ground up - other devs can't simply get rid of your blocking code by wrapping it in async stuff
- Many of the above points and limitations of JS single-threading are why Workers (both web workers and NodeJS Workers) have been introduced
- They basically allow for JS to start using more than one thread, with separate event loops on each
- This allows devs to "offload" expensive/blocking code to a worker thread, to make sure it doesn't block the main thread!
Syntax can be tricky!
Sometimes the syntax of promise / async / await can be tricky to follow, especially when combined with arrow functions and other enclosure types.
Extra reading on this:
- NodeJS.org - Don't Block the Event Loop
- Flaviocopes - JS Event Loop
- MDN - event loop
- https://stackoverflow.com/a/47622472
- I like this quote: "Promises don't run at all. They are simply a notification system for communicating when asynchronous operations are complete."