Dataslope logoDataslope

Promises and async/await

From callbacks to Promises, and finally to async/await — the modern way to write async code that reads top-to-bottom.

The previous chapter taught you the idea of asynchronous code using plain callbacks. Modern JavaScript wraps that idea in two much nicer tools:

  • Promises — objects that represent a future value.
  • async/await — syntax that lets you write Promise-based code as if it were synchronous.

By the end of this chapter, you'll be able to read and write real-world async code.

A Promise is a placeholder

When you start a slow operation, a Promise is the thing you immediately get back. It's a placeholder for a value that isn't ready yet. Later, the Promise will either be:

  • fulfilled with a value, or
  • rejected with an error.

Once a Promise is settled (fulfilled or rejected), it never changes.

Creating and consuming a Promise

You usually consume promises returned by other functions, but occasionally you create one yourself.

Code Block
JavaScript ES2023+

new Promise((resolve, reject) => { ... }) calls your function immediately, passing in two functions. You call resolve(value) when the work succeeds, or reject(error) if it fails.

You consume a promise with .then() (for success), .catch() (for failure), and .finally() (always).

Code Block
JavaScript ES2023+

Chaining replaces nesting

Remember callback hell? Here it is with Promises instead.

Code Block
JavaScript ES2023+

No more rightward drift. Each .then returns a new promise, so chains compose naturally. A single .catch at the end catches any error in any step.

A vital rule: return the next promise from inside .then. If you don't, the chain has no idea to wait for it.

async/await: promise-based code that reads top-to-bottom

async/await is just syntactic sugar over Promises — but it's the syntax most modern code uses.

  • A function marked async always returns a Promise.
  • Inside an async function, await somePromise pauses the function until that promise settles, then evaluates to its fulfilled value (or throws its rejection).
Code Block
JavaScript ES2023+

That code does the same thing as the previous example, but it reads like a recipe. This is why async/await quickly became the default style.

Error handling with try/catch

await throws on rejected promises, so you can use plain try/catch.

Code Block
JavaScript ES2023+

Try, catch, finally — the same shapes you already know from synchronous code. That's the magic of async/await: async code that looks like the synchronous code you've been writing all along.

Running things in parallel: Promise.all

await waits for one thing at a time. Sometimes you want to start several operations at once and wait for all of them.

Code Block
JavaScript ES2023+

sequential takes ~900ms; inParallel takes ~300ms. The difference is when you await: starting all the promises before the first await lets the host work on them concurrently.

A common bug: writing the parallel version like this —

const a = await fakeFetch("/a", 300);
const b = await fakeFetch("/b", 300);

— which is sequential, because each await blocks before the next call starts.

Promise.allSettled, Promise.race, Promise.any

A quick tour of the other combinators:

Code Block
JavaScript ES2023+

When you need to call several independent APIs, Promise.all (or allSettled if you want to keep going past errors) is almost always what you want.

Common pitfalls

A short list of "gotchas" that catch beginners.

Forgetting to await

Code Block
JavaScript ES2023+

If a variable holding "the value" turns out to be a Promise, you forgot an await.

Using await inside a forEach

forEach ignores the promise that an async arrow returns, so your code doesn't wait.

Code Block
JavaScript ES2023+

The rule: when you need to await inside iteration, prefer a plain for...of loop (sequential) or Promise.all(arr.map(...)) (parallel).

Unhandled rejections

A rejected promise that has no .catch (and isn't awaited in a try/catch) becomes an unhandled rejection. Always handle errors somewhere — at the top of your chain at minimum.

Mental model: await is a "pause point"

When the engine hits await expression, it:

  1. Evaluates expression. If it isn't a Promise, wraps it in Promise.resolve(...).
  2. Suspends the async function.
  3. Attaches the rest of the function as a microtask reaction.
  4. Returns to whatever called the async function.
  5. Later, when the promise settles, the microtask runs and the function continues at the line after await.

Holding this picture in mind makes the printed-output puzzles predictable.

Multi-file: a tiny client

Code Block
JavaScript ES2023+

Challenge

Challenge
JavaScript ES2023+
Retry an async operation

Write an async function retry(fn, attempts) that calls fn() (which returns a Promise).

  • If it fulfils, return its value.
  • If it rejects, try again, up to attempts total times.
  • If all attempts fail, reject with the last error.
  • Assume attempts is a positive integer.

Hints:

  • Use a for loop with try/catch.
  • Keep track of the last error so you can throw it at the end.

QuestionSelect one

Which snippet runs the two fetches concurrently (overlapping in time)?

Hint: look for the version where both fetch calls are started before any await pauses execution.

On this page