1. Courses
  2. Developer Foundations
  3. JavaScript Deep Dive
  4. Stop 7

40 minAdvanced

Async JavaScript: callbacks, promises and async/await

Almost everything your team's code waits for is slow: the network, the database, a file, a payment gateway. This lesson shows how JavaScript waits without freezing, how to run waits in parallel safely, and how to stop waiting when something hangs.

By the end you can

  • Explain a promise's three states and what then, catch and finally do
  • Write async/await code with correct error handling, and spot an unhandled rejection
  • Choose between sequential awaits, Promise.all, allSettled, race and any
  • Add a real timeout with AbortSignal.timeout, a retry, and a concurrency limit
  • Find the three most common async bugs in a code review

The everyday picture

You order dinner on a food delivery app such as Uber Eats, Swiggy or Foodpanda. The app does not make you stare at the screen until the food arrives. It gives you an order number right away and says "preparing". You put the phone down and do other things. Later the order ends in one of two ways: the rider arrives with the food, or the order is cancelled and you get a reason. Either way, the app stops showing the spinner.

That order number is a promise: a receipt for a result that does not exist yet. It starts as "pending", and it ends exactly once, as "delivered" or "cancelled". You can attach instructions to it in advance ("when it arrives, call me"; "if it is cancelled, order from somewhere else"). And if you order a main course, a drink and a dessert from three restaurants, you do not wait for the main course before ordering the drink. You order all three at once, and you are done when the slowest one arrives.

Callbacks, and why teams moved on

JavaScript runs your code on one thread (The event loop). When it starts something slow, such as a network request, it cannot sit and wait, or the whole app or API would freeze. So the oldest pattern is to pass a : a function the runtime calls when the work is done. Node's classic style is "error first": the callback gets (err, result), and you must check err yourself.

One callback is fine. Three steps that depend on each other give you code that drifts to the right, checks errors at every level, and is hard to follow. Teams call this . Compare the two versions below: the same three steps, read the store, read its sales, save a report.

JavaScript
// hell.js: three steps that depend on each other, with callbacks
import { getStore, getSales, saveReport } from './store-callbacks.js';

getStore('S014', (err, store) => {
  if (err) return console.error('store failed:', err.message);
  getSales(store, (err, sales) => {
    if (err) return console.error('sales failed:', err.message);
    const totalCents = sales.reduce((sum, c) => sum + c, 0);
    saveReport(totalCents, (err) => {
      if (err) return console.error('save failed:', err.message); // every level checks err
      console.log('saved', totalCents);
    });
  });
});
Output
save failed: disk full
report failed: disk full

util.promisify wraps an error-first callback function so it returns a promise instead. Most modern APIs already return promises: fetch, node:fs/promises, Prisma, every NestJS service you will meet. You still see callbacks in event listeners (button.addEventListener, server.on('request')), and that is fine, because an event can happen many times. A promise is for one result.

Loading the rest of the lesson…