Chapter 9 of 18 · Node.js for Beginners — Complete Free Tutorial

Errors and Debugging in Node

Every program fails; professional ones fail usefully. The Node.js for Beginners series now covers the error toolkit: the error types Node throws, try/catch across synchronous and asynchronous code, crash handlers for the failures that escape everything, and the debugging techniques that turn a stack trace into an answer. Chapter 11 will wire these habits into a real server.

The error taxonomy

KindExampleCaused by
SyntaxErrorUnexpected tokenThe code cannot parse — found at load time
ReferenceErrorx is not definedUsing a name that does not exist
TypeErrorCannot read properties of undefinedWrong type where a value was required
System errorsENOENT, EADDRINUSEThe OS refused an operation (err.code explains)
Operational vs programmerPort busy vs typoExpected in production vs a bug to fix

The operational/programmer split matters most: EADDRINUSE is a condition your app should handle and report; a TypeError is a defect you should fix, not paper over.

try/catch, synchronous and awaited

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

async function loadConfig(path) {
  try {
    const raw = await readFile(path, 'utf8');
    return JSON.parse(raw);
  } catch (err) {
    if (err.code === 'ENOENT') return { theme: 'light' };  // sensible default
    console.error('config problem:', err.message);
    throw err;                                             // unknown → caller decides
  }
}

console.log(await loadConfig('missing.json'));

Expected output:

{ theme: 'light' }

One try around several awaits works because a rejected promise throws inside the async function. The shape — handle what you can, re-throw what you cannot — is the core discipline of error handling.

Custom errors carry meaning

class ValidationError extends Error {
  constructor(field, message) {
    super(message);
    this.name = 'ValidationError';
    this.field = field;
    this.status = 400;       // HTTP status the API should answer with
  }
}

try {
  throw new ValidationError('title', 'title is required');
} catch (err) {
  console.log(err.name, err.field, err.status);
  console.log(err.message);
}

Expected output:

ValidationError title 400
title is required

Subclassing Error keeps stack traces intact and lets callers branch on err.name or instanceof. Attaching status now saves the API layer in chapter 15 from translating errors by hand.

The last-resort handlers

Rejections without a catch and exceptions outside any try would otherwise kill the process silently. Register handlers at startup:

process.on('unhandledRejection', (reason) => {
  console.error('Unhandled rejection:', reason);
  process.exitCode = 1;
});

process.on('uncaughtException', (err) => {
  console.error('Uncaught exception:', err.stack);
  process.exit(1);   // state may be inconsistent — restart is safest
});

Expected output: with these installed, a stray rejection prints a clean diagnostic instead of Node's default fatal trace. Current Node exits on unhandled rejections by default — treating them as fatal is the modern, correct behaviour; the handler exists to log context first.

Reading a stack trace

node app.js

Expected output:

/Users/asha/app.js:4
  total = items.reduce((s, i) => s + i.price);   // i is undefined on one row
          ^
TypeError: Cannot read properties of undefined (reading 'price')
    at totals (/Users/asha/app.js:4:11)
    at Object.<anonymous> (/Users/asha/app.js:9:9)
    at Module._compile (node:internal/modules/cjs/loader:1105:14)

Work from the top down: the error name and message say what went wrong, the first frame in your own file says where, and the frames below explain how you got there. Once the shape of a trace is familiar, most bugs turn into a thirty-second exercise rather than a thirty-minute hunt.

  • Read the top line of your own code first — the frames below are library plumbing.
  • The column number points at the failing expression, not the line start.
  • node --inspect app.js opens the Chrome DevTools debugger: breakpoints, watch, call stack.
  • Ruthless console.log of the actual value (or console.dir(obj, { depth: null })) beats guessing when the shape is unexpected.

Common mistakes

  • catch that swallows — empty handlers hide bugs; log, translate, or re-throw.
  • Catching everything at once — one giant try around unrelated work loses which step failed.
  • Branching on message text — messages change and localise; branch on err.code or err.name.
  • Async errors outside any catch — floating promises reject into the void and crash production.
  • Debugging with alert habits — Node has no dialogs; use logging, stack traces and the inspector.

Practice checklist

  1. Throw and catch each of ReferenceError, TypeError and ENOENT; note their shapes.
  2. Create a custom NotFoundError with a status of 404 and catch it by instanceof.
  3. Deliberately create an unhandled rejection and observe the default fatal behaviour.
  4. Run a buggy file under node --inspect, set a breakpoint and inspect a variable.
  5. Convert a nested callback pyramid into async/await with a single try/catch.

Errors are handled — now for the protocol that defines nearly every Node job. Next: How HTTP Works.

Quiz: Errors and Debugging in Node

5 questions · 70% to pass

Frequently Asked Questions

Node.js is a runtime that executes JavaScript outside the browser. It combines Google's V8 engine with APIs for files, networks, processes and streams, plus a package ecosystem (npm). You use it to build servers, command-line tools, APIs and anything that needs JavaScript running on a machine rather than in a page.

JavaScript is the language; Node.js is one environment that runs it. In the browser the language comes with a DOM, events and window APIs — in Node those are replaced by the file system, network and process modules. Syntax, functions, arrays, promises and async/await are identical, so your JavaScript knowledge transfers directly.

Browser code is sandboxed: it cannot read your files or open arbitrary ports. Node code has full machine access, which is why it is used for servers and tools but also why you never run untrusted code in it. There is no document or window in Node — the equivalents are global, process and modules.

npm is JavaScript's package manager and public registry. package.json describes your project: name, entry point, dependencies with version ranges, and scripts such as npm run dev. Installing a package copies it into node_modules and records the exact intent in package-lock.json so every machine installs identical versions.

Express is a minimal web framework for Node: a few thousand lines that give you routing, middleware, request parsing and static files on top of the raw HTTP server. It is the most common starting point for REST APIs and the backbone of this tutorial's project chapters.

Both work. CommonJS (require/module.exports) is the classic Node format; ES modules (import/export) are the standard and activated in a package.json with "type": "module". Modern projects favour ESM for browser compatibility, but you will read both in existing code, so this course teaches each.

Middleware are functions that run between the incoming request and the final response, in order: body parsing, logging, authentication, rate limits, error handling. Each receives the request, response and next function and can respond itself or pass control along — Express's pipeline model in one sentence.

A JSON file is perfect for learning CRUD: no setup, readable data, easy to reset. The moment you need concurrent writes, querying or multiple users, move to a real database — MySQL/PostgreSQL for relational data or MongoDB for documents. The storage chapter isolates persistence behind one module so swapping it later is straightforward.

A REST API exposes resources over HTTP using verbs on URLs: GET /api/todos lists, POST creates, PUT or PATCH updates, DELETE removes, and everything answers with JSON plus correct status codes (200, 201, 400, 404, 500). It is stateless — each request carries its own authentication — which makes it easy to cache, scale and consume from any frontend.

Node apps are long-running processes, not static files: deploy them to a Node-friendly host (a VPS, Render, Railway or Fly.io), keep secrets in environment variables, terminate HTTPS at a reverse proxy or platform edge, and send logs somewhere durable. The final chapter covers the full checklist including health checks and graceful shutdown.