Joshua's Docs - JS Promises, Async, and Await - Cheatsheet

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

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
  • 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.
  • 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"

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() and then() both pass values with them, whereas finally() 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.
    • 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?
  • 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: 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:
          myAsyncFunction().catch((error)=>{
          	// Handle error
          });
          And it would effectively "capture" all errors that fall through from inside the async function, including any awaits that failed.

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!!!
  • 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 execute Promise.all(promiseArr). This is incorrect; they actually start as soon as you construct them.
    • If you have blocking code in a promise, this can mean that it can start blocking before you have even called Promise.all
  • 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:

Markdown Source Last Updated:
Mon Oct 03 2022 16:31:56 GMT+0000 (Coordinated Universal Time)
Markdown Source Created:
Wed Aug 21 2019 00:46:11 GMT+0000 (Coordinated Universal Time)
© 2024 Joshua Tzucker, Built with Gatsby
Feedback