# Error Handling in JavaScript

Every program makes assumptions. It assumes the network is available. It assumes the user gave you a valid input. It assumes the file exists. It assumes the API response has the shape you expect.

Most of the time those assumptions hold. Then they don't and what happens next is entirely up to you.

JavaScript gives you a system for managing failures without letting them crash the whole program. Understanding it isn't just about catching errors. It's about writing code that fails gracefully code that tells you exactly what went wrong, cleans up after itself, and gives the user something useful instead of a blank screen or a frozen UI.

## What errors actually are in JavaScript

When JavaScript encounters something it can't handle dividing by something illegal, calling a method on `undefined`, trying to parse malformed JSON it creates an `Error` object and throws it. This throw unwinds the call stack, skipping everything below it, until something catches it or it reaches the top level and crashes your program.

```javascript
const user = null;
console.log(user.name); // TypeError: Cannot read properties of null
```

That `TypeError` is a real object. It has a `message` (what went wrong), a `name` (the type of error), and a `stack` (the trace of function calls that led here). JavaScript creates it, throws it, and your program stops unless something is listening for it.

JavaScript has several built-in error types:

*   `TypeError` (wrong type)
    
*   `ReferenceError` (variable doesn't exist)
    
*   `SyntaxError` (invalid code)
    
*   `RangeError` (value out of bounds).
    

They all inherit from the base `Error` class.

## The `try/catch` block

`try/catch` lets you put risky code in a guarded zone. If anything throws inside `try`, execution jumps immediately to `catch` and you decide what to do with the error instead of letting it crash your program.

```javascript
try {
  const data = JSON.parse("this is not valid json");
  console.log(data);
} catch (error) {
  console.error("Failed to parse:", error.message);
}

// Failed to parse: Unexpected token 'h', "this is n"... is not valid JSON
```

Without `try/catch`, that `JSON.parse` throws a `SyntaxError` and everything stops. With it, you catch the error, log it clearly, and your program keeps running.

The `error` object passed to `catch` is whatever was thrown. It has two properties you'll use constantly:

```javascript
try {
  undeclaredVariable;
} catch (error) {
  console.log(error.name);    // "ReferenceError"
  console.log(error.message); // "undeclaredVariable is not defined"
}
```

## The `finally` block

`finally` is a block that runs no matter what whether the `try` succeeded, whether the `catch` triggered, whether an error was re-thrown. It always runs.

```javascript
function loadData() {
  showSpinner();

  try {
    const result = fetchSomething();
    display(result);
  } catch (error) {
    showErrorMessage(error.message);
  } finally {
    hideSpinner(); // always runs — success or failure
  }
}
```

`hideSpinner()` runs whether `fetchSomething` succeeded or crashed. Without `finally`, you'd have to call `hideSpinner()` in both the `try` and the `catch`, duplicating cleanup logic. `finally` exists precisely for this: teardown, cleanup, resource release anything that must happen regardless of outcome.

Here's the full execution order laid out clearly:

![](https://cdn.hashnode.com/uploads/covers/6950d1ca85602739c40abd67/86b2e517-4fcb-49d2-b925-896d3f8b052f.png align="center")

## Throwing custom errors

JavaScript lets you throw anything but throwing a proper `Error` object (or a subclass of it) is best practice because it gives you a stack trace and a message.

```javascript
function divide(a, b) {
  if (b === 0) {
    throw new Error("Cannot divide by zero");
  }
  return a / b;
}

try {
  const result = divide(10, 0);
} catch (error) {
  console.error(error.message); // "Cannot divide by zero"
}
```

You're not waiting for JavaScript to fail you're proactively saying "this situation is invalid" and throwing your own error. The caller's `catch` block handles it just like any built-in error.

For larger applications, creating custom error classes makes your error handling more precise you can catch specific error types and respond differently:

```javascript
class ValidationError extends Error {
  constructor(message, field) {
    super(message);
    this.name = "ValidationError";
    this.field = field;
  }
}

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

function processForm(data) {
  if (!data.email.includes("@")) {
    throw new ValidationError("Invalid email address", "email");
  }
}

try {
  processForm({ email: "notanemail" });
} catch (error) {
  if (error instanceof ValidationError) {
    console.log(`Validation failed on field: ${error.field}`);
    highlightField(error.field);
  } else if (error instanceof NetworkError) {
    console.log(`Network failed with status: ${error.statusCode}`);
    showRetryButton();
  } else {
    console.error("Unexpected error:", error);
  }
}
```

`instanceof` lets you branch on error type different failures get different responses instead of one generic "something went wrong" message.

## Error handling with async/await

`try/catch` works exactly the same way with `async/await`. Rejected promises become catchable errors:

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

    if (!response.ok) {
      throw new NetworkError("Request failed", response.status);
    }

    const user = await response.json();
    return user;
  } catch (error) {
    if (error instanceof NetworkError) {
      console.error(`HTTP ${error.statusCode}: ${error.message}`);
    } else {
      console.error("Unexpected error:", error.message);
    }
    return null;
  } finally {
    hideLoadingIndicator();
  }
}
```

Same underlying problem. Completely different user experience. Error handling is how you translate a technical failure into a useful human message.

A pattern worth building into any serious application:

```javascript
async function safeLoadDashboard() {
  try {
    const [user, stats, notifications] = await Promise.all([
      fetchUser(),
      fetchStats(),
      fetchNotifications()
    ]);

    renderDashboard(user, stats, notifications);
  } catch (error) {
    console.error("Dashboard load failed:", error);
    renderErrorState("Unable to load dashboard. Please refresh.");
  } finally {
    hideGlobalSpinner();
  }
}
```

If any of the three fetches fail, `catch` handles it.
