Menu
CoddyTech

Node.js Error Handling Interview Questions

Where an error goes in callbacks, promises, emitters and the process, and what a service should do with it.

10 questions
  1. Can try/catch catch an error from an asynchronous callback?

    JavaScript
    const log = [];
    
    function loadLater(callback) {
      setTimeout(() => callback(new Error('late')), 0);
    }
    
    try {
      loadLater((err) => {
        log.push('callback: ' + err.message);
        console.log(log.join(', '));
      });
      log.push('try done');
    } catch (err) {
      log.push('caught');
    }

    try/catch covers only code that runs synchronously inside it. When the timer fires, the try has finished, so the callback's error never reaches the catch. That is why callback APIs pass errors as an argument. With promises, await brings the error back into the try.

  2. How do you handle errors with async/await in Node.js?

    Wrap the await in try/catch: a rejected promise throws at the await. Put cleanup in finally.

    JavaScript
    async function fetchUser(id) {
      if (id <= 0) throw new Error(`invalid id ${id}`);
      return { id, name: 'Ada' };
    }
    
    async function handler(id) {
      try {
        const user = await fetchUser(id);
        return `found ${user.name}`;
      } catch (err) {
        return `failed: ${err.message}`;
      } finally {
        console.log(`request ${id} finished`);
      }
    }
    
    async function main() {
      console.log(await handler(7));
      console.log(await handler(-1));
    }
    main();

    finally runs before the promise settles, so request 7 finished prints before found Ada. Catch only where you can act; let the rest reach a central handler. See error handling in async code.

  3. Is return the same as return await inside try/catch?

    JavaScript
    async function fail() {
      throw new Error('boom');
    }
    
    async function withoutAwait() {
      try {
        return fail();
      } catch {
        return 'caught';
      }
    }
    
    async function withAwait() {
      try {
        return await fail();
      } catch {
        return 'caught';
      }
    }
    
    async function main() {
      const a = await withoutAwait().catch((e) => 'rejected: ' + e.message);
      const b = await withAwait();
      console.log(a, '|', b);
    }
    main();

    return fail() returns the rejected promise without waiting, so the function leaves the try before it rejects. return await rejects inside the try, so it is caught. Inside try/catch, always return await.

  4. What happens to an unhandled promise rejection in Node.js?

    Since Node 15 the process crashes with exit code 1, like an uncaught exception; before 15 it only warned.

    process.on('unhandledRejection', (reason) => {
      console.error('unhandled rejection:', reason);
      process.exitCode = 1;
      // log, flush, then shut down; do not carry on as if nothing happened
    });
    
    async function save() {
      throw new Error('db down');
    }
    save(); // no await, no .catch(): this is the bug
    

    The handler is for logging and shutdown. The fix is at the call site: await the promise or add .catch().

  5. Should you use process.on('uncaughtException') to keep a server running?

    No. Log and exit: after an uncaught exception the state is unknown (a half-done transaction, a held lock), and Node's documentation says resuming is unsafe.

    process.on('uncaughtException', (err) => {
      console.error('fatal:', err);
      process.exit(1); // the process manager starts a clean instance
    });
    

    Let a supervisor (Kubernetes, systemd, PM2) restart it, and run several instances.

  6. What happens when an emitter emits 'error' with no listener?

    JavaScript
    const EventEmitter = require('node:events');
    const job = new EventEmitter();
    let result;
    
    try {
      job.emit('error', new Error('disk full'));
      result = 'emitted';
    } catch (err) {
      result = 'thrown: ' + err.message;
    }
    console.log(result);

    With no 'error' listener, emit('error') throws; other events with no listener are ignored. A missing stream.on('error', ...) is a common production crash, so attach one or use pipeline.

  7. What is the difference between operational errors and programmer errors?

    Operational errors are expected failures of a correct program: the database is down, input is invalid, a request timed out. Handle them: retry, return a 4xx or 5xx. Programmer errors are bugs, such as reading a property of undefined; log, crash, restart and fix the code. Branch on err.code (ENOENT, ECONNRESET), never on messages.

  8. How do you create custom error classes in Node.js?

    Extend Error, set name, add fields such as status, and pass the original error as cause (Node 16.9 and later).

    JavaScript
    class AppError extends Error {
      constructor(message, { status = 500, cause } = {}) {
        super(message, { cause });
        this.name = this.constructor.name;
        this.status = status;
      }
    }
    
    class NotFoundError extends AppError {
      constructor(what, options) {
        super(`${what} not found`, { status: 404, ...options });
      }
    }
    
    try {
      try {
        JSON.parse('{bad json');
      } catch (err) {
        throw new NotFoundError('config', { cause: err });
      }
    } catch (err) {
      console.log(err.name, err.status, err.message);
      console.log(err instanceof AppError, err.cause instanceof SyntaxError);
    }

    Handlers branch on instanceof or status, not on message text. See error types.

  9. How do you add a timeout to an async operation?

    Race the operation against a rejecting timer, and clear the timer afterwards.

    JavaScript
    function withTimeout(promise, ms) {
      let timer;
      const timeout = new Promise((_, reject) => {
        timer = setTimeout(() => reject(new Error(`timed out after ${ms} ms`)), ms);
      });
      return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
    }
    
    const sleep = (ms, value) => new Promise((resolve) => setTimeout(() => resolve(value), ms));
    
    async function main() {
      console.log(await withTimeout(sleep(10, 'fast result'), 100));
      try {
        await withTimeout(sleep(200, 'slow result'), 50);
      } catch (err) {
        console.log(err.message);
      }
    }
    main();

    Promise.race stops the waiting, not the work. Where cancellation is supported, pass a signal: fetch(url, { signal: AbortSignal.timeout(5000) }).

  10. Does the second argument of .then catch an error thrown in its first argument?

    JavaScript
    const log = [];
    Promise.resolve()
      .then(
        () => {
          throw new Error('in then');
        },
        () => log.push('second argument'),
      )
      .catch((err) => log.push('catch: ' + err.message))
      .then(() => console.log(log.join(', ')));

    It prints catch: in then. The second argument of .then handles a rejection of the promise before it, not an error thrown by the first argument. That error rejects the promise .then returns, so it skips ahead to the next .catch. End a chain with one .catch instead of passing two callbacks.

Coddy programming languages illustration

Learn to code with Coddy

Get started