Een Node.js applicatie die lokaal perfect draait kan in productie op de meest onverwachte manieren stukgaan. Error handling op schaal in Node.js is het verschil tussen een service die een incident overleeft en eentje die bij de eerste hobbel een downtime veroorzaakt van twintig minuten.
Als je applicatie honderden of duizenden requests per seconde verwerkt, is elke niet-afgevangen fout een potentieel risico. Een onverwachte database timeout, een externe API die traag reageert of een corrupte payload: zonder een doordachte strategie stapelen kleine problemen zich op tot grote storingen.
In deze gids bekijken we hoe je error handling opzet voor productie-omgevingen. We behandelen operationele versus programmeerfouten, centrale handlers, async/await patronen, logging, retries en graceful shutdowns.
Waarom error handling op schaal anders is
Op je laptop met één gebruiker tegelijk voelt een crash niet dramatisch. Je herstart nodemon en gaat verder. In productie gelden andere regels.
Eén niet-afgevangen promise rejection kan een heel Node.js proces laten crashen. Als je load balancer vervolgens requests naar een omvallend proces blijft sturen, krijgen duizenden gebruikers tegelijk een 502. Dat kost vertrouwen én omzet.
Bovendien is debuggen lastiger op schaal. Je hebt geen console-venster om stack traces te lezen. Je hebt gestructureerde logs, metrics en traces nodig om te achterhalen waarom request #48.291 om 03:14 UTC faalde.
Een solide basis leg je door eerst de fundamenten goed te krijgen. Heb je die nog niet gelezen? Begin dan met async programming in Node.js en middleware patterns voor de Express-kant van het verhaal.
Operationele fouten versus programmeerfouten
Voordat je handlers schrijft, moet je begrijpen welke soorten fouten er zijn. Het onderscheid komt oorspronkelijk uit de Node.js documentatie over error handling en helpt je de juiste strategie kiezen.
Operationele fouten zijn voorspelbare problemen die optreden tijdens normale werking:
- Een database connectie die timeouten krijgt
- Een externe API die een 503 teruggeeft
- Ongeldige gebruikersinvoer
- Een bestand dat niet bestaat
- Rate limiting door een third party
Deze fouten handel je af: je logt ze, geeft een nette response en probeert eventueel een retry.
Programmeerfouten zijn bugs in je code:
TypeError: Cannot read property 'x' of undefined- Een oneindige loop
- Verkeerde typeconversies
- Memory leaks
Bij programmeerfouten is je applicatie in een onbekende staat. Doorgaan is riskant, een graceful shutdown en herstart is veiliger dan de fout proberen te verbergen.
Een centrale error handler in Express
In Express koppel je een error handler aan het einde van je middleware-stack. Deze middleware heeft vier parameters, wat Express vertelt dat het een error handler is.
app.use((err, req, res, next) => {
const statusCode = err.statusCode || 500;
const isOperational = err.isOperational || false;
req.log.error({
err,
requestId: req.id,
path: req.path,
method: req.method,
userId: req.user?.id,
}, 'Request failed');
if (!isOperational) {
// Programmeerfout: flag voor shutdown
process.emit('criticalError', err);
}
res.status(statusCode).json({
error: {
message: isOperational ? err.message : 'Internal server error',
code: err.code,
requestId: req.id,
},
});
});
Het idee: je onderscheidt bekende (operationele) fouten van onbekende fouten. Bij bekende fouten stuur je de message terug naar de client. Bij onbekende fouten verberg je details om geen stack traces of interne paden te lekken.
Een eigen Error class gebruiken
Een gestructureerde Error class maakt dit onderscheid eenvoudiger:
class AppError extends Error {
constructor(message, statusCode, code) {
super(message);
this.statusCode = statusCode;
this.code = code;
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
// Gebruik:
throw new AppError('Gebruiker niet gevonden', 404, 'USER_NOT_FOUND');
Combineer dit met input validatie via Zod en je hebt meteen een consistente manier om 400-achtige fouten aan de grens van je applicatie te vangen.
Async errors correct afvangen
Een veelgemaakte fout is dat async errors stilletjes verdwijnen. Express 4 vangt geen fouten uit async route handlers automatisch af. Express 5 doet dit wel, maar veel codebases draaien nog op 4.
// Fout: deze error komt niet bij je error handler terecht
app.get('/users/:id', async (req, res) => {
const user = await userService.find(req.params.id);
res.json(user);
});
Als userService.find gooit, krijg je een unhandled rejection. Los dit op met een wrapper:
const asyncHandler = (fn) => (req, res, next) =>
Promise.resolve(fn(req, res, next)).catch(next);
app.get('/users/:id', asyncHandler(async (req, res) => {
const user = await userService.find(req.params.id);
if (!user) throw new AppError('Niet gevonden', 404, 'NOT_FOUND');
res.json(user);
}));
Of gebruik een library zoals express-async-errors die dit globaal oplost door de router te patchen.
Process-level handlers
Naast request-level errors heb je process-level handlers nodig voor fouten die ontsnappen aan je middleware.
process.on('unhandledRejection', (reason, promise) => {
logger.error({ reason, promise }, 'Unhandled Rejection');
process.emit('criticalError', reason);
});
process.on('uncaughtException', (err) => {
logger.fatal({ err }, 'Uncaught Exception');
// Niet herstellen: state is onbekend
gracefulShutdown(1);
});
Een uncaughtException betekent bijna altijd een bug. De veilige keuze is loggen en herstarten. Een process manager zoals PM2, Docker of Kubernetes brengt je service daarna weer omhoog.
Gestructureerde logging voor error context
Op schaal is console.log niet genoeg. Je wilt gestructureerde JSON-logs die je kunt filteren op request ID, gebruiker of service.
Populaire keuzes zijn Pino en Winston. Pino is razendsnel en produceert standaard JSON:
import pino from 'pino';
const logger = pino({ level: process.env.LOG_LEVEL || 'info' });
logger.error({
err,
requestId,
userId,
duration: Date.now() - start,
}, 'Payment failed');
Belangrijke vuistregels:
- Log altijd het volledige error object, niet alleen
err.message. Je mist anders de stack trace. - Voeg context toe: request ID, user ID, endpoint, duration.
- Gebruik log levels bewust:
errorvoor operationele fouten,fatalvoor crash-waardige situaties,warnvoor verdachte maar niet-brekende events. - Redact gevoelige data: wachtwoorden, tokens, creditcardnummers. Pino heeft hiervoor een
redactoptie.
Combineer dit met JWT-gebaseerde authentication om gebruikers-id's veilig aan je logs te koppelen zonder tokens te lekken.
Retries, timeouts en circuit breakers
Niet elke fout hoeft direct naar de gebruiker. Een tijdelijke hickup bij een externe API los je op met een retry. Maar ongedoseerd retryen verergert cascading failures.
Exponential backoff
async function retryWithBackoff(fn, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (err) {
if (attempt === maxRetries || !isRetryable(err)) throw err;
const delay = Math.min(1000 * 2 ** attempt, 10000);
await new Promise(r => setTimeout(r, delay + Math.random() * 200));
}
}
}
De random jitter (Math.random() * 200) voorkomt dat duizenden clients tegelijk opnieuw proberen, een fenomeen dat het herstel van de backend juist tegenwerkt.
Circuit breakers
Als een dependency structureel faalt, is doorgaan met retryen zinloos. Een circuit breaker (bijvoorbeeld via de opossum library) opent na een aantal mislukkingen en wijst nieuwe calls direct af tot de dependency herstelt.
Timeouts altijd instellen
const response = await fetch(url, {
signal: AbortSignal.timeout(5000),
});
Een call zonder timeout is een request die eeuwig kan hangen. Op schaal betekent dit uitgeputte connectiepools en dalende throughput. Dit is extra belangrijk wanneer je werkt met databases via connection pools.
Graceful shutdown
Als je proces moet stoppen, door een deploy, een crash of een signaal, wil je in-flight requests netjes afronden.
let shuttingDown = false;
async function gracefulShutdown(exitCode = 0) {
if (shuttingDown) return;
shuttingDown = true;
logger.info('Shutdown gestart');
server.close(() => logger.info('HTTP server gesloten'));
await Promise.race([
Promise.all([db.close(), redis.quit()]),
new Promise(r => setTimeout(r, 10000)),
]);
process.exit(exitCode);
}
process.on('SIGTERM', () => gracefulShutdown(0));
process.on('SIGINT', () => gracefulShutdown(0));
Kubernetes stuurt SIGTERM bij een pod-restart. Sluit binnen terminationGracePeriodSeconds (standaard 30s), anders krijg je een SIGKILL. Een health check endpoint dat tijdens shutdown 503 teruggeeft helpt de load balancer om geen nieuwe requests meer te sturen.
Error monitoring en alerting
Loggen alleen is niet genoeg, je moet weten wanneer iets stukgaat zonder elke tien minuten door logs te scrollen. Tools als Sentry, Datadog en New Relic nemen dit uit handen.
Waar je op wilt alerten:
- Error rate boven een drempelwaarde (bijvoorbeeld >1% van requests)
- Nieuwe error types die nooit eerder voorkwamen
- Latency spikes in specifieke endpoints
- Unhandled rejections en uncaught exceptions (altijd kritiek)
- Dependency failures: database, cache, externe APIs
Koppel alerts aan de juiste kanalen. Een development error hoeft niet om 03:00 iemand te wekken; een productie outage wel.
Veelgemaakte fouten
- Alleen
err.messageloggen, je verliest de stack trace. - Errors opeten met lege
catchblocks, bugs verdwijnen, maar de symptomen blijven. - Interne details in responses tonen, stack traces naar de client is een security risico.
- Geen onderscheid tussen 4xx en 5xx, een ongeldige input is geen server error.
- Retries zonder limiet of backoff, je maakt het probleem erger.
- Process blijven draaien na
uncaughtException, je werkt met corrupte state. - Geen request ID, correleren van logs over services wordt onmogelijk.
Een goede REST API zet correcte statuscodes en foutformaten centraal. Zie onze gids over REST API design best practices voor concrete conventies.
Checklist voor productie
Voor je naar productie gaat:
- Centrale error handler middleware aanwezig
-
unhandledRejectionenuncaughtExceptionhandlers geregistreerd - Custom
AppErrorclass metisOperationalflag - Async routes gewrapped of Express 5 in gebruik
- Gestructureerde logging met request ID
- Timeouts op alle externe calls
- Retries met exponential backoff en jitter waar zinvol
- Circuit breakers voor kritieke dependencies
- Graceful shutdown op SIGTERM/SIGINT
- Error tracking tool geconfigureerd
- Alerts op error rate en nieuwe error types
- Gevoelige data redacted in logs
Veelgestelde vragen
Wat is error handling op schaal in Node.js?
Error handling op schaal betekent dat je fouten consistent vangt, logt en afhandelt in een productieomgeving met veel verkeer. Het omvat centrale handlers, gestructureerde logging, monitoring en herstelstrategieën zoals retries en circuit breakers.
Wat is het verschil tussen operationele en programmeerfouten?
Operationele fouten zijn voorspelbare fouten zoals een database die offline is of een ongeldige gebruikersinvoer. Programmeerfouten zijn bugs in je code, zoals een undefined property aanroepen. Operationele fouten handel je af; bij programmeerfouten is een restart vaak veiliger.
Moet ik altijd een process crash laten gebeuren bij een uncaught exception?
Ja, bij een uncaughtException is de applicatie in een onbekende staat. Log de fout, doe een graceful shutdown en laat een process manager zoals PM2 of Kubernetes de service herstarten. Doorgaan kan leiden tot memory leaks en corrupte data.
Hoe voorkom ik dat één request mijn hele Node.js server laat crashen?
Gebruik een centrale error handler middleware in Express, wrap async functies zodat errors naar next() gaan, en vang zowel synchrone als asynchrone fouten af. Combineer dit met input validatie en timeouts op externe calls om cascading failures te voorkomen.
Welke tools helpen bij error tracking in Node.js productie?
Populaire tools zijn Sentry, Datadog, New Relic en open source opties zoals OpenTelemetry met Grafana. Ze bieden stack traces, context over requests, alerts en integraties met je logging pipeline. Kies op basis van je team, budget en bestaande stack.
Tot slot
Error handling op schaal gaat niet om het voorkomen van elke fout, dat is onmogelijk. Het gaat erom dat fouten zichtbaar, begrijpelijk en herstelbaar zijn. Met een centrale handler, gestructureerde logs, verstandige retries en een graceful shutdown leg je de basis voor een betrouwbare service.
Begin met de basis: één error class, één middleware, één logger. Voeg daarna observability, retries en circuit breakers toe naarmate je verkeer en complexiteit groeien. Elke stap maakt je applicatie iets minder fragiel.