Error Handling in JavaScript: try/catch and Advanced Techniques in Node.js

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Error Handling in JavaScript: try/catch and Beyond: Advanced Level



Error handling is not just about preventing application crashes; it is a fundamental aspect of building reliable and fault-tolerant systems in JavaScript and Node.js. Basic use of try/catch is familiar to every developer, but in real-world projects, especially on the server side, many nuances arise: asynchronous flows, promise chains, global failures, and the need for structured custom errors. In this article, we will explore advanced error handling techniques that will help you write cleaner and more predictable code in Node.js.



1. Deep Dive into try/catch and Asynchronicity



Classic try/catch is synchronous. It works perfectly for catching errors inside regular functions but fails with asynchronous callbacks or promises without await. In Node.js, where 90% of code is asynchronous, this often leads to "swallowed" exceptions.



Mistake: try/catch does not catch an error in setTimeout

function dangerousAsync() {  setTimeout(() => {    throw new Error('Timer failure!');  }, 1000);}

try { dangerousAsync();} catch (e) { console.log('Caught:', e.message); // This code will NOT execute}


In this example, the error is thrown outside the try context, and the Node.js process will crash. The correct approach is to use promises or async/await.



Solution: wrap asynchronous code in a Promise

function safeAsync() {  return new Promise((resolve, reject) => {    setTimeout(() => {      reject(new Error('Timer failure!'));    }, 1000);  });}

(async () => { try { await safeAsync(); } catch (e) { console.log('Caught:', e.message); // Will execute after 1 second }})();


The main rule: wrap any asynchronous operation in a promise, and use await inside try/catch. This ensures the error is caught in the correct context.



2. Custom Error Classes: Semantics and Context



Instead of throwing new Error('Something went wrong'), create a hierarchy of your own error classes. This allows the handler to understand the type of failure and respond accordingly (e.g., log, send to monitoring, or return a specific HTTP status to the user).



class AppError extends Error {  constructor(message, statusCode = 500) {    super(message);    this.name = this.constructor.name;    this.statusCode = statusCode;    Error.captureStackTrace(this, this.constructor);  }}

class ValidationError extends AppError { constructor(message) { super(message, 400); }}

class DatabaseError extends AppError { constructor(message, originalError) { super(message, 503); this.originalError = originalError; // Keep the original error for debugging }}

// Usagefunction findUser(id) { if (!id) throw new ValidationError('User ID is required'); // ... logic that might throw a DatabaseError}


Advantages of this approach:

  • Readability: the class name immediately indicates the nature of the problem.
  • Routing: in a global handler, you can check instanceof and choose a response strategy.
  • Additional fields: you can add any data (statusCode, originalError, userId, etc.).


3. Global Error Handlers in Node.js



Even with the most careful control, unhandled errors can leak out. In Node.js, there are two key events for catching them: uncaughtException and unhandledRejection

Blogs

Book Recommendations