Skip to main content

Command Palette

Search for a command to run...

Error Handling in JavaScript: Try, Catch, Finally

How to catch, manage, and learn from the inevitable — without letting your application crash and burn.

Published
7 min read
Error Handling in JavaScript: Try, Catch, Finally

How to catch, manage, and learn from the inevitable — without letting your application crash and burn.

1. What Are Errors in JavaScript?


Every JavaScript program, no matter how well written, will eventually encounter an error. An error is an unexpected condition that disrupts the normal flow of execution. JavaScript distinguishes between two fundamental kinds of problems you'll face.

Syntax Errors

These occur when the JavaScript engine cannot parse your code at all — they're caught before a single line runs. A missing bracket, a misspelled keyword, or an unexpected token will throw a SyntaxError.

syntax error example

// Missing closing parenthesis — caught immediately by the engine
console.log("Hello World"  // ← SyntaxError: Unexpected end of input

Runtime Errors

These are the tricky ones. The code looks fine, so it parses successfully — but something goes wrong during execution. Accessing a property on undefined, calling something that isn't a function, or dividing by zero — these all cause runtime errors.

/ ReferenceError — variable doesn't exist
console.log(username); // ReferenceError: username is not defined

// TypeError — wrong type for an operation
const user = null;
console.log(user.name); // TypeError: Cannot read properties of null

// RangeError — value out of acceptable range
const arr = new Array(-1); // RangeError: Invalid array length
Error Type When It Occurs
SyntaxError Malformed code that can't be parsed
ReferenceError Accessing an undeclared variable
TypeError Wrong type used in an operation
RangeError Value outside allowed range
URIError Malformed URI functions
EvalError Issues with the eval() function

02. Usingtryandcatch


The try...catch statement is JavaScript's primary mechanism for handling runtime errors gracefully. You place potentially dangerous code inside a try block. If an error is thrown, execution immediately jumps to the catch block — and your program keeps running instead of crashing.

try {
  // Code that might throw an error
  const data = JSON.parse(invalidJson);
} catch (error) {
  // Runs only if an error was thrown above
  console.error("Something went wrong:", error.message);
}

The catch block receives the error object as its parameter. This object carries useful properties you can inspect:

reading the error object

try {
  const user = null;
  console.log(user.name);
} catch (error) {
  console.log(error.name);    // "TypeError"
  console.log(error.message); // "Cannot read properties of null"
  console.log(error.stack);   // Full stack trace string
}

A Real-World Example

Fetching data from an API is a classic scenario where errors can occur — the network might be down, the response might be malformed, or the server might return unexpected data.

async function getUserData(id) { try { const response = await fetch(/api/users/${id});

if (!response.ok) {
  throw new Error(`HTTP error: ${response.status}`);
}

const user = await response.json();
return user;
} catch (error) {
 console.error("Failed to fetch user:", error.message); 
return null; // Graceful fallback 
    }
 }

⚠ Note

try...catch only handles synchronous errors and awaited async errors. A plain Promise rejection without await will slip past an unguarded catch block.

03. ThefinallyBlock


function readFile(path) {
  let fileHandle = null;

  try {
    fileHandle = openFile(path); // risky operation
    return fileHandle.read();

  } catch (error) {
    console.error("Read failed:", error.message);
    return null;

  } finally {
    // Runs even if try returned or catch ran
    if (fileHandle) fileHandle.close();
    console.log("File handle released.");
  }
}
💡
Key Insight: Common uses for finally: closing database connections, stopping loading spinners, releasing locks, or logging completion — anything that must happen regardless of outcome.

04. Throwing Custom Errors


JavaScript lets you throw any value, but the cleanest approach is to extend the built-in Error class. Custom error types let you be precise about what kind of thing went wrong — a failed validation is not the same as a missing resource.


"Be specific about failure. A well-named error is worth more than a thousand console logs."


custom error classes

// Define custom error types
class ValidationError extends Error {
  constructor(message, field) {
    super(message);
    this.name = "ValidationError";
    this.field = field;
  }
}

class NotFoundError extends Error {
  constructor(resource) {
    super(`${resource} was not found`);
    this.name = "NotFoundError";
    this.statusCode = 404;
  }
}

// Use them with throw
function validateAge(age) {
  if (typeof age !== "number") {
    throw new ValidationError("Age must be a number", "age");
  }
  if (age < 0 || age > 150) {
    throw new ValidationError("Age is out of range", "age");
  }
  return true;
}

// Catch and handle by type
try {
  validateAge(-5);
} catch (error) {
  if (error instanceof ValidationError) {
    console.error(`Field "\({error.field}" failed: \){error.message}`);
  } else {
    throw error; // Re-throw unknown errors
  }
}

Notice the pattern of re-throwing unknown errors. Your catch block should handle what it understands and let everything else propagate. Silently swallowing all errors is one of the most common — and most dangerous — mistakes in error handling.

05. Why Error Handling Matters


Graceful Failure vs. Crashing

Without error handling, a single thrown error terminates your entire script. A user clicking a button gets a frozen page and no feedback. With proper error handling, you can show a helpful message, retry the operation, fall back to cached data, or log the issue and keep the rest of the UI working.

Debugging Benefits

Structured error handling makes bugs easier to find. When you attach meaningful messages, capture the stack property, and log errors to a monitoring service, you receive rich context — not just "something broke." You know where it broke, what the inputs were, and how execution reached that point.

function processOrder(order) {
  try {
    validateOrder(order);
    chargeCard(order.payment);
    fulfillOrder(order);

  } catch (error) {
    // Log structured context — far more useful than console.log("error")
    logger.error({
      message: error.message,
      type: error.name,
      orderId: order?.id,
      stack: error.stack,
      timestamp: new Date().toISOString()
    });

    // Show user-friendly feedback
    showToast("Order failed. Please try again.");
  }
}

Security Considerations

What you don't show matters as much as what you do. Never expose raw error messages or stack traces to end users — they can reveal your application's internal structure. Log full details server-side; show safe, generic messages client-side.

✅ Best Practices

Always handle errors as close to their source as possible. Use custom error classes for domain-specific failures. Never silently swallow errors with empty catch blocks. Use finally for cleanup. Log with context, not just a message string.

Quick Reference

  • try { } — Wrap code that might throw a runtime error

  • catch(error) { } — Runs only when an error is thrown; receives the error object with .name, .message, and .stack

  • finally { } — Always runs after try/catch, perfect for cleanup code

  • throw new Error(msg) — Manually throw an error from any point in your code

  • Custom errors — Extend Error to create typed, descriptive errors; use instanceof to handle them selectively

  • Re-throw unknown errors — Never silently swallow errors you didn't expect; let them propagate

  • Async errors — Use try/catch with async/await, or .catch() on Promise chains