Node.js
Exception Handling
Best Practices
Web Development
Programming

Node.js Best Practice Exception Handling

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

Good exception handling in Node.js is less about catching everything everywhere and more about handling errors at the right layer. Synchronous exceptions, rejected promises, framework middleware, and process-level crash handlers each have a different role, and mixing them up usually leads to hidden failures or unstable recovery behavior.

Handle Errors Close to Where They Occur

For synchronous code, try and catch are still the correct tool.

javascript
1function parsePort(raw) {
2  const port = Number(raw);
3
4  if (!Number.isInteger(port)) {
5    throw new Error("Port must be an integer");
6  }
7
8  return port;
9}
10
11try {
12  console.log(parsePort("3000"));
13} catch (error) {
14  console.error("Invalid configuration:", error.message);
15}

This works for code that throws during the current call stack. It does not automatically handle asynchronous failures that happen later in callbacks or promise chains.

Use async and await with Local try and catch

In modern Node.js code, most application-level error handling happens around await.

javascript
1async function loadUser(id) {
2  const response = await fetch(`https://api.example.com/users/${id}`);
3
4  if (!response.ok) {
5    throw new Error(`User request failed: ${response.status}`);
6  }
7
8  return response.json();
9}
10
11async function main() {
12  try {
13    const user = await loadUser(42);
14    console.log(user);
15  } catch (error) {
16    console.error("Failed to load user:", error.message);
17  }
18}
19
20main();

This style keeps error handling readable and avoids deeply nested callback logic. It also makes it clear which function owns recovery and which one simply reports failure upward.

Let Framework Middleware Centralize Request Errors

In web servers, request-level errors should usually flow into one central handler rather than being logged and responded to separately in every route.

javascript
1import express from "express";
2
3const app = express();
4
5app.get("/users/:id", async (req, res, next) => {
6  try {
7    const user = await loadUser(req.params.id);
8    res.json(user);
9  } catch (error) {
10    next(error);
11  }
12});
13
14app.use((error, req, res, next) => {
15  console.error(error);
16  res.status(500).json({ message: "Internal server error" });
17});

This keeps route handlers focused on business logic and gives you one place for logging, status mapping, and response format decisions.

Use Custom Error Types for Expected Failures

Not every error is the same. A validation problem, missing record, and database outage should not all be treated identically.

javascript
1class ValidationError extends Error {}
2class NotFoundError extends Error {}
3
4function requireEmail(email) {
5  if (!email) {
6    throw new ValidationError("Email is required");
7  }
8}
9
10function getUserOrThrow(user) {
11  if (!user) {
12    throw new NotFoundError("User not found");
13  }
14  return user;
15}

Typed errors make it easier to map failures cleanly at API boundaries without pattern-matching on message strings.

Process-Level Handlers Are for Logging and Shutdown

Node provides unhandledRejection and uncaughtException, but they should be treated as last-resort crash hooks, not as a normal recovery strategy.

javascript
1process.on("unhandledRejection", (reason, promise) => {
2  console.error("Unhandled rejection:", reason);
3});
4
5process.on("uncaughtException", (error) => {
6  console.error("Uncaught exception:", error);
7  process.exit(1);
8});

The important operational rule is that an uncaught exception means the process may be in an undefined state. In practice, these handlers are good for synchronous cleanup, final logging, and then exiting so a supervisor can restart the service.

Log Enough Context, But Do Not Hide the Failure

A useful error handler should capture:

  • the error object or stack
  • the request or job context
  • identifiers that help you correlate logs

What it should not do is quietly swallow the failure and keep going as if nothing happened. Silent recovery is often more damaging than an explicit crash, because it leaves the system in a bad state without a clear signal.

For background jobs, the equivalent principle is to fail the job clearly, log the context, and let retry logic or orchestration decide what happens next.

Common Pitfalls

The most common mistake is assuming try and catch will automatically handle asynchronous failures that are not awaited correctly. Another is using global process handlers as if they were a safe place to continue normal operation after a crash. Current Node.js guidance is much stricter: uncaughtException is a last-resort cleanup hook, not a resume point. Developers also lose useful observability by logging only error.message and discarding the stack or request context.

Summary

  • Handle synchronous exceptions locally with try and catch.
  • Use async and await with local handling for asynchronous workflows.
  • Centralize HTTP request errors in framework middleware.
  • Use custom error types when different failures need different responses.
  • Treat unhandledRejection and uncaughtException as crash-level signals for logging, cleanup, and shutdown, not normal recovery.

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.