De node async await-flow lijkt op het eerste gezicht verwarrend: callbacks, Promises en async/await lijken allemaal hetzelfde werk op verschillende manieren te doen. In werkelijkheid zijn het drie generaties van hetzelfde idee. Node.js draait op één thread, en bewerkingen zoals een bestand lezen, een database bevragen of een HTTP-verzoek doen kosten tijd. In deze gids leer je waarom asynchrone flow nodig is, hoe we van callback hell naar Promises en vervolgens naar de schone syntaxis van async/await gingen, hoe je fouten afhandelt en hoe je werk parallel uitvoert.
Waarom asynchroon? Een snelle blik op de event loop
Node.js draait op één hoofdthread en is gebouwd rond het principe om die thread nooit te blokkeren. Een bestand van schijf lezen duurt milliseconden; als Node die hele tijd simpelweg zou wachten, kon het ondertussen geen enkel ander binnenkomend verzoek verwerken. In plaats daarvan draagt het de bewerking over aan het besturingssysteem, gaat door met ander werk en komt terug zodra het resultaat klaar is. Dit mechanisme heet de event loop.
Een belangrijk onderscheid: asynchrone code betekent niet threads die parallel draaien. Er is één thread; alleen de wachttijden worden slim beheerd. Daarom blokkeert een lange CPU-berekening (bijvoorbeeld een enorme lus) toch de event loop — async/await helpt daar niet; het is alleen ontworpen voor bewerkingen die op I/O wachten.
Eerste generatie: callbacks en de "callback hell"
In de begindagen van Node werden asynchrone bewerkingen beheerd met callback-functies. Het resultaat van een bewerking komt terug via een functie die je als argument meegeeft. De conventie van Node is de "error-first callback": de eerste parameter is de fout, de tweede het resultaat.
const fs = require("fs");
fs.readFile("a.txt", "utf8", (err, data) => {
if (err) return console.error(err);
console.log(data);
});
Voor één bewerking is dat prima. Maar zodra bewerkingen van elkaar afhankelijk worden, verandert de code in een piramide die naar rechts afdrijft. Dit heet callback hell:
readFile("a.txt", (err, a) => {
readFile("b.txt", (err, b) => {
readFile("c.txt", (err, c) => {
// foutcontrole herhaalt zich op elk niveau
});
});
});
Deze structuur is moeilijk te lezen, moeilijk qua foutafhandeling en moeilijk te onderhouden. Promises kwamen precies om dit probleem op te lossen.
Tweede generatie: wat is een Promise?
Een Promise is een object dat "een waarde die nog niet klaar is maar in de toekomst arriveert" voorstelt. Het heeft drie toestanden: pending (in afwachting), fulfilled (succesvol voltooid) en rejected (geëindigd met een fout). Je vangt het resultaat op met .then() en de fout met .catch():
const fs = require("fs/promises");
fs.readFile("a.txt", "utf8")
.then((data) => console.log(data))
.catch((err) => console.error(err));
De echte kracht van Promises is chaining (ketenen). Elke .then() geeft een nieuwe Promise terug, dus in plaats van een piramide krijg je een platte flow. Toch daalt de leesbaarheid bij heel lange ketens — en daar komt async/await in beeld.
Derde generatie: synchroon ogende code met node async await
async/await is syntactische suiker (syntactic sugar) bovenop Promises; eronder draaien nog steeds Promises. Als je een functie met async markeert, kun je er await in gebruiken. await "wacht" op die regel tot een Promise zich oplost, maar het blokkeert de event loop niet.
const fs = require("fs/promises");
async function lees() {
const a = await fs.readFile("a.txt", "utf8");
const b = await fs.readFile("b.txt", "utf8");
return a + b;
}
Deze code doet hetzelfde werk als de callback-piramide, maar leest van boven naar beneden, als synchrone code. Drie regels om te onthouden:
awaitkan alleen binnen eenasync-functie gebruikt worden (of, in modern Node, op het topniveau van modules).- Een
async-functie geeft altijd een Promise terug; wat je er ookreturnt, de aanroeper ontvangt het viaawaitof.then(). awaitversnelt niet alleen I/O; CPU-zwaar werk blokkeert nog steeds de hoofdthread.
Foutafhandeling: schoon opvangen met try/catch
Bij callbacks moest je op elk niveau if (err) schrijven. Met async/await gebruik je try/catch net als in gewone, synchrone code. Dit is een van de grootste praktische voordelen van async/await:
async function lees() {
try {
const data = await fs.readFile("ontbreekt.txt", "utf8");
return data;
} catch (err) {
console.error("Lezen mislukt:", err.message);
return null;
}
}
Als een await-expressie wordt afgewezen (rejected), wordt het catch-blok geactiveerd. Als je de fout niet opvangt, wordt het een unhandled rejection, wat in moderne Node-versies het proces kan laten crashen. Elke asynchrone bewerking heeft dus een foutstrategie nodig: ofwel een lokale try/catch, ofwel een Promise-afwijzing die hogerop door de aanroeper wordt opgevangen.
Parallel uitvoeren: Promise.all en allSettled
Een veelgemaakte fout is om onafhankelijke bewerkingen zonder reden na elkaar af te wachten. De code hieronder leest twee bestanden sequentieel; de tweede wacht tot de eerste klaar is:
const a = await fs.readFile("a.txt", "utf8"); // deze eindigt eerst
const b = await fs.readFile("b.txt", "utf8"); // dan start deze
Als de bewerkingen niet van elkaar afhankelijk zijn, is beide tegelijk starten en samen afwachten veel sneller. Daarvoor gebruik je Promise.all:
const [a, b] = await Promise.all([
fs.readFile("a.txt", "utf8"),
fs.readFile("b.txt", "utf8"),
]);
Promise.all wordt in zijn geheel afgewezen als zelfs maar één van zijn bewerkingen afgewezen wordt. Als je sommige bewerkingen wilt laten falen en toch elk resultaat wilt zien, gebruik dan Promise.allSettled; die geeft de status (fulfilled/rejected) van elke bewerking afzonderlijk terug.
Veelgemaakte fouten
awaitmetforEachin een lus.array.forEach(async ...)werkt niet zoals je verwacht;forEachwacht niet op Promises. Gebruik een klassiekefor...ofom sequentieel te wachten, ofPromise.all(array.map(...))om parallel uit te voeren.awaitvergeten. Zonderawaitbevat de variabele een onopgelost Promise-object, niet de waarde.- Onnodig sequentieel wachten. Onafhankelijke bewerkingen één voor één afwachten vertraagt je verzoeken zonder reden.
Veelgestelde vragen
Moet ik async/await of .then() gebruiken?
Beide gebruiken dezelfde Promise-basis; de keuze gaat over leesbaarheid. In flows met veel afhankelijke stappen leest async/await meestal schoner. Voor één korte transformatie kan .then() praktisch zijn. Consistent blijven binnen dezelfde codebase is het beste.
Vertraagt await de code?
await op zichzelf vertraagt de code niet; het stelt alleen de voortgang van de functie uit tot die Promise zich oplost, en de event loop blijft ondertussen ander werk doen. Traagheid komt meestal door onafhankelijke bewerkingen zonder reden sequentieel af te wachten — parallelliseer ze met Promise.all.
Is async/await genoeg voor CPU-zwaar werk?
Nee. async/await beheert alleen I/O-wachttijden efficiënt. Een zware berekening (encryptie, beeldverwerking) blokkeert nog steeds de hoofdthread. Daarvoor moet je de worker_threads-module of een apart proces overwegen.
Wil je de asynchrone flow in je Node.js-project opschonen? Om een codebase die callback hell is geworden met async/await op te ruimen, prestaties te verbeteren met parallellisme, of vanaf nul een solide API te bouwen, neem contact met me op.