Robust error handling is what separates amateur applications from production-ready systems. In this chapter, you’ll learn how to handle errors gracefully, debug effectively, and implement logging strategies.
Every error in JavaScript inherits from the built-in Error class. When you create an error, Node.js automatically captures a stack trace — a breadcrumb trail showing exactly which functions were called and in what order leading up to the error. This stack trace is your most valuable debugging tool.
// Built-in Error types -- each signals a different category of problemconst err1 = new Error('Something went wrong'); // Generic errorconst err2 = new TypeError('Expected a string'); // Wrong type usedconst err3 = new RangeError('Value out of range'); // Number outside valid rangeconst err4 = new SyntaxError('Invalid syntax'); // Malformed code or JSON// Error propertiesconsole.log(err1.message); // 'Something went wrong' -- human-readable descriptionconsole.log(err1.name); // 'Error' -- the constructor nameconsole.log(err1.stack); // Stack trace -- shows the call chain that led here
// Basic try/catchasync function getData() { try { const response = await fetch('/api/data'); if (!response.ok) { throw new AppError('Failed to fetch data', response.status); } return await response.json(); } catch (error) { if (error.isOperational) { // Handle operational error console.error('Operational error:', error.message); } else { // Re-throw programming errors throw error; } }}// Wrapper function for cleaner code -- eliminates the need for try/catch// in every single route handler. This is a higher-order function: it takes// your async handler, runs it, and if the returned promise rejects,// automatically forwards the error to Express's next().const asyncHandler = (fn) => (req, res, next) => { Promise.resolve(fn(req, res, next)).catch(next);};// Usage in Expressapp.get('/users/:id', asyncHandler(async (req, res) => { const user = await User.findById(req.params.id); if (!user) throw new NotFoundError('User'); res.json(user);}));
These are your application’s last line of defense — safety nets that catch errors which slipped through every other handler. In a well-designed app, these should rarely fire. If they fire frequently, you have unhandled errors upstream that need fixing.Think of these like the emergency shutoff on a factory machine: you hope they never activate, but when they do, they prevent catastrophic damage.
// Uncaught exceptions (synchronous code) -- fires when a throw statement// reaches the top of the call stack without being caught.// The process is in an undefined state after this, so exiting is the only safe option.process.on('uncaughtException', (err) => { console.error('UNCAUGHT EXCEPTION! Shutting down...'); console.error(err.name, err.message); console.error(err.stack); process.exit(1); // Let your process manager (PM2, Docker) restart the app});// Unhandled promise rejections -- fires when an async operation rejects// and no .catch() or try/catch is waiting for it.// Starting with Node.js 15+, unhandled rejections crash the process by default.process.on('unhandledRejection', (reason, promise) => { console.error('UNHANDLED REJECTION! Shutting down...'); console.error(reason); // Close server gracefully (finish handling in-flight requests), then exit server.close(() => { process.exit(1); });});// SIGTERM handling -- sent by orchestrators (Heroku, Docker, Kubernetes)// when they want your process to stop. You get a brief window to finish// current work before the process is forcefully killed (SIGKILL).process.on('SIGTERM', () => { console.log('SIGTERM received. Shutting down gracefully'); server.close(() => { console.log('Process terminated'); });});
Production tip: Always run your Node.js process behind a process manager like PM2 or inside a container orchestrator. When process.exit(1) fires, the manager automatically restarts the process, minimizing downtime. Without a process manager, your app just stays dead after a crash.
# Built-in debuggernode inspect app.js# Chrome DevToolsnode --inspect app.js# Then open chrome://inspect in Chrome# Break on first linenode --inspect-brk app.js
console.log is fine during development, but in production you need structured, level-based logging that can write to files, external services, or monitoring platforms. Winston is the most widely used logging library in the Node.js ecosystem. It separates the concept of what you log (levels and messages) from where it goes (transports like console, files, or cloud services).