# Error Handling in JavaScript: Try, Catch, Finally

## Introduction

You write some code. It looks right. You run it, and instead of the output you expected, the browser throws a red wall of text and the whole script stops dead. That's a runtime error, and if you haven't planned for it, your user just saw a broken page.

JavaScript has a structured way to handle this: the `try...catch...finally` block. It's not glamorous, but understanding it properly will save you a lot of late-night debugging.

* * *

## What Errors Actually Are

JavaScript has several built-in error types. You'll run into these most often:

*   `ReferenceError`: accessing a variable that doesn't exist
    
*   `TypeError`: calling something that isn't a function, or reading a property off `null`
    
*   `SyntaxError`: malformed code (usually caught before runtime)
    
*   `RangeError`: a value that falls outside an allowed range
    

Here's a classic example:

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

JavaScript tries to access `.name` on `null`, fails, and throws an error. Without any handling, execution stops right there. Everything below that line never runs.

* * *

## The try and catch Blocks

The `try` block wraps code that might fail. If it does, execution jumps to `catch`, which receives the error as an argument.

```javascript
try {
    const user = null;
    console.log(user.name);
} catch (error) {
    console.log("Something went wrong:", error.message);
}
```

The script no longer crashes. Instead of a red wall of text, you get a controlled message. That's what "graceful failure" means in practice. Not pretending errors don't happen, just deciding what to do when they do.

The `error` object has a few useful properties:

*   `error.message`: a human-readable description
    
*   `error.name`: the type of error (`"TypeError"`, `"ReferenceError"`, etc.)
    
*   `error.stack`: the full call stack, useful for debugging
    

Getting into the habit of logging `error.stack` (not just `error.message`) during development will show you exactly where things went wrong, including the file and line number.

* * *

## The finally Block

`finally` runs no matter what, whether the `try` block succeeded, threw an error, or even hit a `return` statement.

```javascript
function fetchData() {
    try {
        // simulate a risky operation
        const data = JSON.parse("invalid json {{{");
        return data;
    } catch (error) {
        console.log("Failed to parse:", error.message);
    } finally {
        console.log("Cleanup done.");
    }
}
```

The `finally` block is where you put cleanup code: closing a database connection, hiding a loading spinner, releasing a lock. Stuff that needs to happen regardless of whether the operation worked.

A common real-world pattern looks like this:

```javascript
async function loadUser(id) {
    showSpinner(true);
    try {
        const response = await fetch(`/api/users/${id}`);
        const user = await response.json();
        renderUser(user);
    } catch (error) {
        showErrorMessage("Could not load user.");
    } finally {
        showSpinner(false);
    }
}
```

The spinner always gets hidden. It doesn't matter if the fetch succeeded or failed. Without `finally`, you'd need to call `showSpinner(false)` in both the `try` and the `catch`, and you'd inevitably miss a case.

* * *

## Throwing Custom Errors

JavaScript lets you throw your own errors with the `throw` keyword. You can throw a string, a number, or an object, but the cleanest approach is to throw an actual `Error` instance so you get a stack trace.

```javascript
function divide(a, b) {
    if (b === 0) {
        throw new Error("Division by zero is not allowed.");
    }
    return a / b;
}

try {
    const result = divide(10, 0);
} catch (error) {
    console.log(error.message); // "Division by zero is not allowed."
}
```

You can also extend the built-in `Error` class to create named custom errors. This is useful when you want to handle different error types differently:

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

function validateAge(age) {
    if (typeof age !== "number") {
        throw new ValidationError("Age must be a number.");
    }
    if (age < 0 || age > 120) {
        throw new ValidationError("Age must be between 0 and 120.");
    }
}

try {
    validateAge("twenty");
} catch (error) {
    if (error instanceof ValidationError) {
        console.log("Validation failed:", error.message);
    } else {
        throw error; // re-throw unexpected errors
    }
}
```

Notice the re-throw at the end. If the error isn't a `ValidationError`, you probably don't know how to handle it, so let it bubble up. Swallowing all errors silently is one of the more frustrating bugs to track down, because nothing tells you something went wrong.

* * *

## Why Error Handling Matters

There are three practical reasons to get this right.

### Your users get a better experience

A caught error can show a friendly message or a fallback UI. An uncaught one shows nothing, or breaks the page mid-render.

### Debugging gets easier

When you log errors consistently, you build a trail. Adding something like `console.error(error.stack)` in your catch blocks gives you a line number and a call path, not just a vague failure.

### Your app keeps running

An unhandled error in one place can cascade and break unrelated parts of the page. Containing it means the rest of your code still works.

## Handling Async Err

One thing worth knowing: `try...catch` only catches synchronous errors by default. For async code, you need to use it inside an `async` function with `await`, or chain `.catch()` on a promise. Forgetting this is a common source of "my error handler isn't catching anything" confusion.

```javascript
// This does NOT catch the rejection:
try {
    fetch("/api/data"); // not awaited
} catch (error) {
    console.log("This never runs");
}

// This DOES:
try {
    const response = await fetch("/api/data");
} catch (error) {
    console.log("Network error:", error.message);
}
```

* * *

## Conclusion

Error handling isn't about anticipating every possible failure. It's about deciding, ahead of time, what your code should do when something goes wrong. The `try` block says "try this." The `catch` block says "if it breaks, do this instead." The `finally` block says "either way, do this."

That structure alone handles most of the cases you'll encounter. Custom errors and re-throwing handle the rest.
