1.2Asynchronous: callbacks, Promises, async/await
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.
The pyramid of doom (never do this)
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
- Write an asynchronous operation with a callback (e.g. fs.readFile) and spot the error-first convention (err, data).
- Chain two dependent operations with nested callbacks: observe the start of callback hell.
- Rewrite with Promises and .then().then().catch() chaining: observe the better readability.
- Finally rewrite with async/await (async function, await before operations, try/catch for errors): the code reads like synchronous code.
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.
- 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.
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.
What does await do in an async function?