Node.js Error Handling

Good Node.js error handling keeps your server running, gives clients useful messages and gives you logs to fix problems. Errors fall into two groups: operational errors you expect (invalid input, missing records, network failures) and programmer bugs (typos, undefined variables).

try/catch with async/await

Handle the errors you understand and rethrow the rest. Silently swallowing every error hides bugs.

import { readFile } from 'node:fs/promises';

async function loadSettings() {
  try {
    return JSON.parse(await readFile('settings.json', 'utf8'));
  } catch (err) {
    if (err.code === 'ENOENT') return { theme: 'light' }; // sensible default
    throw err; // unexpected: let the caller handle it
  }
}
console.log(await loadSettings());
{ theme: 'light' }

Custom Error Classes

export class AppError extends Error {
  constructor(message, status = 500) {
    super(message);
    this.name = 'AppError';
    this.status = status;
  }
}

export class NotFoundError extends AppError {
  constructor(what = 'Resource') {
    super(`${what} not found`, 404);
  }
}

const err = new NotFoundError('User');
console.log(err.message, err.status, err instanceof AppError);
User not found 404 true

Express Error-Handling Middleware

Error middleware has four parameters: (err, req, res, next). Register it after all routes. Calling next(err) anywhere, or throwing inside an async handler in Express 5, sends control to it.

app.get('/api/users/:id', async (req, res) => {
  const user = await db.findUser(req.params.id);
  if (!user) throw new NotFoundError('User');
  res.json(user);
});

// 404 for unknown routes
app.use((req, res, next) => next(new NotFoundError('Route')));

// central error handler
app.use((err, req, res, next) => {
  const status = err.status ?? 500;
  if (status >= 500) console.error(err);
  res.status(status).json({
    error: status >= 500 ? 'Something went wrong' : err.message,
  });
});
GET /api/users/999  ->  404 {"error":"User not found"}
GET /nope           ->  404 {"error":"Route not found"}

Async Errors in Express 4

Express 4 does not catch rejected promises. Wrap async handlers so errors reach your error middleware, or upgrade to Express 5.

const asyncHandler = (fn) => (req, res, next) =>
  Promise.resolve(fn(req, res, next)).catch(next);

app.get('/api/orders', asyncHandler(async (req, res) => {
  res.json(await db.orders());
}));
(a database failure now returns 500 instead of crashing or hanging)

Process-Level Safety Nets

As a last resort, log uncaught errors and exit. After an unknown error the process may be in a broken state, so let a process manager (pm2, Docker, systemd) restart it.

process.on('unhandledRejection', (reason) => {
  console.error('Unhandled rejection:', reason);
  process.exit(1);
});
process.on('uncaughtException', (err) => {
  console.error('Uncaught exception:', err);
  process.exit(1);
});
Unhandled rejection: Error: connect ECONNREFUSED 127.0.0.1:5432

Best Practices

  • Never send stack traces to clients in production.
  • Use a structured logger such as pino for searchable logs.
  • Validate input early and return 400 errors with clear messages.
  • Always throw Error objects, not strings, so you keep the stack trace.

Frequently asked questions

How do I handle errors in Express?

Add an error-handling middleware with four arguments (err, req, res, next) after all routes, and pass errors to it with next(err) or by throwing in Express 5 async handlers.

Why does my Express app crash on an async error?

In Express 4, rejected promises in handlers are not caught. Wrap handlers with an asyncHandler helper or upgrade to Express 5, which forwards them automatically.

Should I catch uncaughtException and keep running?

No. Log the error and exit; the app may be in an inconsistent state. Use a process manager to restart it automatically.