Error Handling (try/catch/finally)

Difficulty: Beginner

Question

How does error handling work in JavaScript? Explain try/catch/finally and custom error classes.

Answer

JavaScript uses try/catch/finally blocks for synchronous error handling. The try block contains code that might throw. The catch block handles the error. The finally block runs regardless of whether an error occurred - it is guaranteed to execute even if catch throws or try returns.

JavaScript has several built-in error types: Error, TypeError, ReferenceError, SyntaxError, RangeError, and URIError. Each has a name, message, and stack trace.

Custom error classes extend the built-in Error class to create domain-specific errors. This is essential in production code for distinguishing between different error types (validation errors, authentication errors, not-found errors) and handling each appropriately.

For asynchronous code, try/catch works with async/await. For promises without await, use .catch(). Unhandled rejections should be caught with a global handler.

Code examples

try/catch/finally Behavior

// Basic try/catch/finally
function divide(a, b) {
  try {
    if (b === 0) throw new Error('Division by zero');
    const result = a / b;
    console.log(`Result: ${result}`);
    return result;
  } catch (err) {
    console.log(`Error: ${err.message}`);
    return null;
  } finally {
    console.log('Cleanup: always runs');
  }
}

divide(10, 2);
console.log('---');
divide(10, 0);

// finally runs even after return
function test() {
  try {
    return 'from try';
  } finally {
    console.log('finally still runs!');
  }
}
console.log(test());

finally ALWAYS executes - after return, after throw, after catch. It is perfect for cleanup like closing connections or releasing resources.

Built-in Error Types

// TypeError - wrong type operation
try {
  null.toString();
} catch (e) {
  console.log(`${e.constructor.name}: ${e.message}`);
}

// ReferenceError - accessing undeclared variable
try {
  console.log(undeclaredVar);
} catch (e) {
  console.log(`${e.constructor.name}: ${e.message}`);
}

// RangeError - value out of range
try {
  new Array(-1);
} catch (e) {
  console.log(`${e.constructor.name}: ${e.message}`);
}

// Checking error type
try {
  JSON.parse('{invalid}');
} catch (e) {
  if (e instanceof SyntaxError) {
    console.log('Invalid JSON:', e.message);
  } else {
    throw e; // re-throw unexpected errors
  }
}

Use instanceof to handle specific error types differently. Re-throw errors you do not know how to handle.

Custom Error Classes

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

class ValidationError extends AppError {
  constructor(field, message) {
    super(message, 400);
    this.name = 'ValidationError';
    this.field = field;
  }
}

class NotFoundError extends AppError {
  constructor(resource) {
    super(`${resource} not found`, 404);
    this.name = 'NotFoundError';
  }
}

// Usage in an API handler
function getUser(id) {
  if (!id) throw new ValidationError('id', 'User ID is required');
  if (id !== 1) throw new NotFoundError('User');
  return { id: 1, name: 'Alice' };
}

try {
  getUser(null);
} catch (err) {
  if (err instanceof ValidationError) {
    console.log(`Validation: ${err.field} - ${err.message} (${err.statusCode})`);
  } else if (err instanceof NotFoundError) {
    console.log(`Not found: ${err.message} (${err.statusCode})`);
  }
}

try {
  getUser(99);
} catch (err) {
  console.log(`${err.name}: ${err.message} (${err.statusCode})`);  
}

Custom errors extend Error to add context like status codes and field names. instanceof checks let you handle each type differently in catch blocks.

Key points

Concepts covered

try/catch, finally, Error Types, Custom Errors, Error Propagation