Course topics

By WebNest Studio Engineering Team

JavaScript Tutorial

Error Handling in JavaScript

Errors are inevitable — a network call fails, a user enters invalid input, a value is unexpectedly null. JavaScript provides try/catch/finally for handling exceptions gracefully instead of letting them crash the whole program, along with the built-in Error object (and subclasses) for describing what went wrong.

Writing deliberate error handling — rather than just hoping errors don't happen — is what separates fragile scripts from resilient applications.

Why it matters

Errors will happen: bad input, failed requests, unexpected data. Code that does not handle them shows the user a blank or frozen page. Handling them well means catching what you can recover from, reporting the rest clearly, and never swallowing an error silently so that the problem becomes invisible.

try, catch, and finally

Code that might throw goes inside a try block. If an exception occurs, execution immediately jumps to the matching catch (error) { ... } block, skipping the rest of the try block. The optional finally block runs afterward no matter what happened — whether the try succeeded, an error was caught, or even if the catch block itself re-threw — making it the right place for cleanup like closing a connection or hiding a spinner.

Throwing Custom Error Objects

You can throw any value with throw, but convention (and good debugging) means throwing Error objects, which capture a message, a name, and a stack trace. For domain-specific failures, it's common to create custom error classes that extend Error, so calling code can distinguish error types (e.g. ValidationError vs NetworkError) using instanceof.

Error Boundaries as a Concept

An "error boundary" is a deliberate point in an application where errors are caught and handled gracefully instead of letting them propagate all the way up and crash the app or leave the UI in a broken state. In frameworks like React, this is a literal component type; conceptually, in any JavaScript app, it means wrapping risky operations (rendering, async calls, third-party code) in try/catch at strategic boundaries, logging or reporting the error, and showing a fallback rather than a blank or frozen screen.

Examples

try/catch/finally with a custom Error subclass

JavaScript
class ValidationError extends Error {
  constructor(message) {
    super(message);
    this.name = 'ValidationError';
  }
}

function setAge(age) {
  if (age < 0) {
    throw new ValidationError('Age cannot be negative');
  }
  return age;
}

try {
  setAge(-5);
} catch (err) {
  if (err instanceof ValidationError) {
    console.error('Validation problem:', err.message);
  } else {
    console.error('Unexpected error:', err.message);
  }
} finally {
  console.log('Validation attempt complete.');
}
Output
Validation problem: Age cannot be negative
Validation attempt complete.

A conceptual error boundary function wrapping risky async work

JavaScript
async function withErrorBoundary(operation, fallbackValue) {
  try {
    return await operation();
  } catch (err) {
    console.error('Caught at boundary:', err.message);
    return fallbackValue; // app keeps running with a safe default
  }
}

async function loadSettings() {
  throw new Error('Settings service unreachable');
}

withErrorBoundary(loadSettings, { theme: 'default' }).then((settings) =>
  console.log('Using settings:', settings)
);
Output
Caught at boundary: Settings service unreachable
Using settings: { theme: 'default' }

Common Mistakes

  • Throwing plain strings or objects instead of Error instances, losing the automatically captured stack trace that makes debugging much easier.
  • Wrapping code in try/catch and then silently swallowing the error (an empty catch block) instead of logging it or handling it meaningfully — this hides real bugs.
  • Forgetting that catch only catches synchronous errors and errors from awaited Promises inside the try block — an unhandled rejection in an unawaited async call elsewhere will not be caught.
  • Using finally to return a value, which silently overrides any value returned or error thrown inside the try/catch — a subtle and confusing bug.

Exercise

Define a ValidationError class that extends Error and sets its name. Write parseAge, which converts text to a number and throws a ValidationError with the message age must be a number when the text is not numeric. The code below catches the error and always prints done at the end.

Expected output: 25, ValidationError: age must be a number, done (one per line)

JavaScript · starter code
// TODO: class ValidationError extends Error, with name "ValidationError"

function parseAge(text) {
  // TODO: return the number, or throw a ValidationError if it is not a number
}

try {
  console.log(parseAge("25"));
  console.log(parseAge("abc"));
} catch (error) {
  console.log(error.name + ": " + error.message);
} finally {
  console.log("done");
}
Show hints
  • In the constructor, call super(message) and then set this.name = "ValidationError".
  • Number("abc") gives NaN, which is detected with Number.isNaN.
Show solution
JavaScript · solution
class ValidationError extends Error {
  constructor(message) {
    super(message);
    this.name = "ValidationError";
  }
}

function parseAge(text) {
  const age = Number(text);
  if (Number.isNaN(age)) {
    throw new ValidationError("age must be a number");
  }
  return age;
}

try {
  console.log(parseAge("25"));
  console.log(parseAge("abc"));
} catch (error) {
  console.log(error.name + ": " + error.message);
} finally {
  console.log("done");
}

Quick quiz

1. When does a finally block run?

  • A. Always, whether or not an error was thrown
  • B. Only when an error was thrown
  • C. Only when no error was thrown
  • D. Only if there is no catch block
Show answer

A. It is used for clean-up such as hiding a loading indicator.

2. Can a try...catch around a setTimeout call catch an error thrown inside the timer's callback?

  • A. Yes
  • B. No; the callback runs later, after the try block has finished
  • C. Only in Node.js
  • D. Only for TypeErrors
Show answer

B. The error has to be handled inside the callback, or the code written with promises and await.

3. What should be thrown when something goes wrong?

  • A. A number
  • B. A string
  • C. An Error object, or an instance of a subclass of Error
  • D. null
Show answer

C. An Error carries a message and a stack trace, which a plain string does not.

Interview questions

How do you handle errors in asynchronous code?

Show answer

With promises, attach a catch handler to the end of the chain. With async and await, wrap the awaited calls in try and catch, because a rejected promise is thrown at the await. A plain try block does not catch errors thrown later inside callbacks. Unhandled rejections can be observed globally with the unhandledrejection event, as a last line of defence for logging.

Why create custom error classes?

Show answer

They let the calling code tell kinds of failure apart with instanceof or the name property, and respond differently: show a message for a validation error, retry a network error, report an unexpected one. A custom error can also carry extra data, such as a field name or a status code. Extending Error keeps the message and stack trace.

Key Points to Remember

  • try/catch/finally lets code recover from exceptions instead of crashing; finally always runs for cleanup.
  • Prefer throwing Error objects (or custom subclasses) over raw strings, since they carry a message, name, and stack trace.
  • Custom error classes let calling code distinguish error types with instanceof.
  • An error boundary is a deliberate catch point that keeps the rest of an application usable when one part fails.

Practice in the playground

Write and run your own code for this topic, or start from the exercise or an example. Shows a live preview of your HTML, CSS and JavaScript. Your code for each topic is kept in this browser.

Run the examples

Change an input, predict the result, then compare it with the output. Explain why the result changes.

WebNest Studio

Written by

WebNest Studio Engineering Team

Practical lessons from developers working with Java, Python, React, databases and production systems.

Led by Vansh Duggal, Founder, WebNest Studio · Full-stack & software engineering