1.2 · Asynchronous: callbacks, Promises, async/await

Level 1 · JavaScript & Node basics

1.2Asynchronous: callbacks, Promises, async/await

Goal: understand and handle asynchrony in its three forms (callbacks, Promises, async/await), and favour async/await — the central milestone of Node.
Estimated time: 13 min · self-paced reading · If you only have 5 min: read “The essentials in 30s” + the diagram.

The essentials in 30 seconds

  • Asynchronous = “when it's ready”: slow operations start and their result arrives later — you cannot read it on the next line.
  • Callbacks → callback hell: the error-first convention (error as 1st argument) works, but dependent callbacks nest into an unreadable pyramid.
  • Favour async/await: await waits without blocking, code reads like synchronous code, errors via try/catch — built on Promises.

Running mini-case — Salim waits for his files in Algiers

Salim must read a configuration file, then query a database with its content, then call an API: he writes three nested callbacks and gets lost in indentation — worse, his console.log shows undefined because he reads the result before it arrives. He rewrites everything with async/await and try/catch: three readable await lines in order, and the error handled in a single place. The undefined bug disappears along with the old mental model.

Continuation of case 1.1 — Salim moves from the language to the heart of Node: asynchrony.

1. From callbacks to callback hell

Asynchrony is THE central concept of Node: slow operations (reading a file, querying a database, calling an API) start and their result arrives later. The historical mechanism is the callback: a function passed as an argument, called back when the operation ends — with the error-first convention (error as first argument, null if all is well). Chaining dependent operations leads to callback hell: an unreadable pyramid, hard to manage especially for errors.

Salim writes fs.readFile('config.txt', (err, data) => ...) and spots the error-first convention: test err first, use data afterwards — never the reverse.

2. Promises: the chained future value

A Promise represents a future value — the result of an operation not yet finished. Three states: pending (waiting), fulfilled (success, a value), rejected (error). You react with .then() (success) and .catch() (error), and Promises chain instead of nesting — far more readable than callback hell.

Salim rewrites his file reading as fs.promises.readFile(...).then().catch(): the chain replaces the pyramid and the error lands in a single .catch().

3. async/await: readability (preferred)

The real readability revolution is async/await: in an async function, await waits for the result without blocking (syntactic sugar over Promises) and asynchronous code reads like synchronous code, line after line, with a classic try/catch for errors. It is the dominant and recommended style: you will meet all three forms in existing code, but you will mostly write async/await.

Salim finishes with const data = await fs.promises.readFile(...) in a try/catch: three operations in three ordered lines — his asynchronous code finally reads like a recipe.

Common misinterpretation: “await blocks the program like a sleep” or “you can read the result on the line following the call”. No. await only suspends the async function while waiting for the Promise, without blocking the event loop — other requests keep being handled. And the result of an asynchronous operation does not exist yet on the next line: it must be consumed in the callback, the .then() or after an await.
The evolution of asynchrony Towards readability Callbacks error-first, riskof callback hellPromises Future value,.then/.catch chainingasync/await Reads likesynchronous code, try/catch
Towards readability
Text version of the diagram (SEO / screen readers / translation): 1) Launch, don't wait: Salim launches the file reading and gives a “when it's ready” callback (error-first) instead of reading the result on the next line. 2) Chain: he replaces nested callbacks with Promises in .then().catch() — no more pyramid. 3) Flatten: he switches to async/await in a try/catch, three operations in three readable lines. Lesson: distinguishing “launching” from “getting the result” is the mental model that unlocks all of Node.

The pyramid of doom (never do this)

“I chain six nested callbacks, I can still follow… at the 3rd level.”
Verdict: at the 4th level, even the author is lost — and error handling is scattered everywhere.
“My console.log shows undefined, the file must be empty.”
Verdict: the file is full, it's your read that arrives too early — the result doesn't exist yet on the next line.

Morality: nesting is burying yourself — flattening with await is breathing.

🔎 Concrete example — 🔎 Concrete example — The same code with async/await

Readable, non-blocking, handled errors — the style to write.

const fs = require("fs");
async function lire() {
  try {
    const data = await fs.promises.readFile("f.txt", "utf8");
    console.log(data);
  } catch (err) {
    console.error("Failure:", err.message);
  }
}
lire();

Reading: await waits for the content without blocking; on failure, catch prints the message.

Lesson: Promise.all to launch several independent operations in parallel.

Section vocabulary

Callback
Function passed as an argument, called when an asynchronous operation finishes. Node error-first convention: first argument = error (null if OK), then the result.

Simple example: The (err, data) => ... of Salim's file reading.

What it is not: a function called immediately with an already-ready result.

Reflex: test err first, use the result afterwards.

Callback hell
Unreadable nesting of callbacks when chaining dependent asynchronous operations; problem solved by Promises and async/await.

Simple example: The three-level pyramid of Salim's first script.

What it is not: three awaits aligned readably in an async function.

Reflex: as soon as it nests, switch to Promises.

Promise
Object representing a future value. Three states: pending, fulfilled (success), rejected (error). React via .then() and .catch().

Simple example: The promise returned by fs.promises.readFile in Salim's code.

What it is not: the file content available on the next line.

Reflex: .then for success, .catch for error.

async/await
Modern syntax built on Promises: await waits for the result without blocking; asynchronous code reads like synchronous code. Errors via try/catch.

Simple example: The three ordered awaits of Salim's final script.

What it is not: a sleep that would block the whole application.

Reflex: write with async/await by default.

error-first / try-catch
Error handling depending on the form: error as first callback argument, .catch() for Promises, try/catch around awaits.

Simple example: The single try/catch securing Salim's three operations.

What it is not: ignoring the error and letting the application crash silently.

Reflex: always an explicit error path.

In practice — Hands-on — Master asynchrony

  1. Write an asynchronous operation with a callback (e.g. fs.readFile) and spot the error-first convention (err, data).
  2. Chain two dependent operations with nested callbacks: observe the start of callback hell.
  3. Rewrite with Promises and .then().then().catch() chaining: observe the better readability.
  4. Finally rewrite with async/await (async function, await before operations, try/catch for errors): the code reads like synchronous code.
You understand and handle asynchrony in its three forms (callbacks, Promises, async/await), and favour async/await for readable code — the central milestone of Node.

Ready-to-say script — explain asynchrony to a colleague (1 min)

“In Node, a slow operation starts but its result arrives later: you never read it on the next line. You say 'when it's ready, do this' — with a callback, a .then() or an await. That's why we write with async/await: the code waits without blocking and reads in order.”

To say in front of the classic console.log showing undefined bug.

Copy-paste template — async/await file reading

const fs = require("fs");
async function lire(nom) {
  try {
    const data = await fs.promises.readFile(nom, "utf8");
    console.log(data);
    return data;
  } catch (err) {
    console.error("Failure:", err.message);
    return null;
  }
}
lire("f.txt");

Dependent operations

Read a file, then query with its content: chain the awaits in order in a single try/catch — like Salim with config → database → API.

Independent operations

Several reads with no link between them: launch them together with Promise.all to go fast instead of waiting for each in turn.

Legacy callback code

Facing an existing error-first API: understand it and use it as is for reading, but write all new code with async/await.

Silent error in Algiers

Salim systematically adds a catch printing err.message: an unhandled asynchronous error crashes or lies silently — never an await without a safety net.

✓ Best practices

  • Distinguish “launching” from “getting”.
  • Write with async/await by default.
  • Always an explicit error path.

✗ Common pitfalls

  • Reading the result on the next line.
  • Nesting dependent callbacks.
  • await without try/catch.

Key takeaways

  • ASYNCHRONY is THE central concept of Node: slow operations start and their result arrives LATER.
  • Progression: CALLBACKS (error-first) → risk of callback hell → PROMISES (.then/.catch chaining) → ASYNC/AWAIT.
  • A PROMISE = future value (pending/fulfilled/rejected); react with .then() (success) and .catch() (error).
  • ASYNC/AWAIT (preferred): await waits without blocking, code READS like synchronous code, errors via try/catch.

48h challenge — apply tomorrow

Take the template above: read a real file with a callback, rewrite with Promises, then with async/await and try/catch. Compare the three versions. 25 minutes is enough.

Section exit checklist
  • I can explain “launching” vs “getting the result”.
  • I know when to use callback, Promise or await.
  • I wrote the same operation in all three forms.
Self-assessment 1-5: do I spot the bug of a result read too early?is my new code in async/await with try/catch?

Frequently asked questions

Why is asynchrony so confusing at first, and how to really understand it?

It breaks the intuition “each line finishes before the next”. The classic trap: reading the result on the line following the call — you get undefined because the operation hasn't finished yet. The mental model to adopt: distinguish “launching” (immediate) from “getting the result” (later, via callback, .then() or await). Think “when it's ready, do this” — like an order at a restaurant: you don't stand in the kitchen, you carry on and get served when it's ready. Once this clicks, as for Salim, all of Node becomes readable.

Callbacks, Promises or async/await: which one to use today?

You must know all three but write with async/await. Callbacks: the historical form, to understand for reading existing code and classic APIs (error-first convention) — avoid nesting them. Promises: the modern foundation (async/await is built on top) — to understand, and directly useful for parallelism (Promise.all). async/await: your default style for all new code — readable, errors via try/catch. Practical rule: write with await, understand Promises, know how to read callbacks.

Check your understanding

What does await do in an async function?

Additional resources (optional, to go further)

Level 1 examFresh, harder questions to test what you've learned