# Error Handling in JavaScript: Try, Catch, Finally

*Audience: developers with basic JavaScript knowledge — functions, callbacks, and async/await aren't assumed, but you should know what a function call is.*

**TL;DR:** JavaScript errors don't have to crash your program. The `try/catch/finally` block gives you a structured way to intercept failures, respond to them, and always clean up — regardless of what went wrong.

### Problem

Unhandled errors in JavaScript throw uncaught exceptions — they halt execution and give the user nothing useful. Consider fetching user data from an API: the network can fail, the response can be malformed, or the server can be down. Without error handling, any of these causes a silent crash or an unformatted stack trace in the console.

```js
// Without error handling — crash if fetch fails
const response = await fetch("https://video.thitainfo.com/api/health");
const data = await response.json();
console.log(data.status); // Explodes if fetch throws or json() fails
```

The fix is a deliberate error-handling strategy using `try`, `catch`, and `finally`.

* * *

### Solution

#### Step 1 — Wrap risky code in a `try` block

The `try` block contains any code that might throw. If an error occurs anywhere inside it, execution immediately jumps to `catch`.

```js
// Wrapping a network call
async function fetchUser(userId) {
  try {
    const response = await fetch(`https://api.example.com/users/${userId}`);

    if (!response.ok) {
      throw new Error(
        `HTTP ${response.status}: Failed to fetch user ${userId}`,
      );
    }

    const user = await response.json();
    return user;
  } catch (error) {
    console.error("fetchUser failed:", error.message);
    return null;
  }
}

// Usage
const user = await fetchUser(42);
// Output on network failure: fetchUser failed: Failed to fetch
```

The `catch` block receives the error object. `error.message` is the human-readable description; `error.name` tells you the type (e.g. `TypeError`, `SyntaxError`).

* * *

#### Step 2 — Use `finally` for guaranteed cleanup

`finally` always executes — whether the `try` succeeded, `catch` ran, or even if `return` was called mid-block. It's the right place for cleanup: closing connections, hiding loading spinners, releasing locks.

```js
async function fetchUserWithSpinner(userId) {
  showSpinner(); // Start loading UI

  try {
    const response = await fetch(`https://api.example.com/users/${userId}`);

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

    return await response.json();
  } catch (error) {
    console.error("Request failed:", error.message);
    return null;
  } finally {
    hideSpinner(); // Runs no matter what — success or failure
  }
}
```

Without `finally`, a thrown error inside `catch` could leave the spinner visible forever.

* * *

#### Step 3 — Throw custom errors for precise control

JavaScript's built-in `Error` class is generic. For application logic, extend it to create specific error types that carry more context — and that let `catch` blocks distinguish between failure modes.

```js
class ValidationError extends Error {
  constructor(field, message) {
    super(message);
    this.name = "ValidationError";
    this.field = field; // Extra context beyond just the message
  }
}

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

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

// Catch block can now branch by error type
try {
  validateAge("thirty");
} catch (error) {
  if (error instanceof ValidationError) {
    console.error(
      `Validation failed on field '${error.field}': ${error.message}`,
    );
    // Output: Validation failed on field 'age': Age must be a number
  } else {
    throw error; // Re-throw anything unexpected
  }
}
```

Re-throwing is important: only handle what you understand. Silently swallowing all errors (`catch (e) {}`) hides real bugs.

* * *

Now here's the full execution order, so there's no ambiguity about when each block runs:`finally` runs in every path. That's the guarantee worth internalizing.

* * *

### Results

A codebase with structured error handling produces measurably better outcomes:

*   Errors surface with specific messages (`ValidationError on field 'email'`) instead of generic uncaught exceptions, cutting average debugging time significantly on production incidents.
    
*   `finally` blocks eliminate resource leaks — open file handles, visible loading states, database connections — that compound over time in long-running Node.js processes.
    
*   Custom error classes let monitoring tools (Sentry, Datadog) group errors by type rather than by stack trace, making alerting far less noisy.
    

* * *

### Trade-offs

`try/catch` **is not free.** In hot loops (tens of thousands of iterations), wrapping every iteration in `try/catch` can impose a measurable overhead in V8. Move the `try/catch` outside the loop and handle errors at the batch level if performance matters there.

**It doesn't catch everything.** `try/catch` only catches synchronous errors and errors in `await`\-ed promises. An unhandled promise rejection (`fetch(...).then(...)` without `.catch()`) won't be caught by a surrounding `try/catch`. Use `async/await` consistently so `await` surfaces rejections into the nearest `catch`.

**Overly broad catch blocks hide bugs.** `catch (e) { /* do nothing */ }` is almost always wrong. At minimum, log the error. Better: only catch errors you can meaningfully handle, and re-throw the rest.

* * *

### Conclusion

Error handling isn't defensive programming — it's the contract your code makes with everyone calling it. `try` marks what can fail, `catch` defines the recovery, `finally` enforces cleanup. Custom error classes give you the precision to distinguish user mistakes from infrastructure failures. Start with `try/catch` around any I/O, validate inputs with custom errors, and use `finally` wherever resources need releasing.

* * *

### Further reading

*   [MDN — try...catch](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/try...catch) — complete syntax reference
    
*   [MDN — Error](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error) — all built-in error types and how to extend them
    
*   [Jake Archibald — Tasks, microtasks, queues and schedules](https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/) — essential context for understanding where async errors surface
    
*   [Node.js — Error handling best practices](https://nodejs.org/en/learn/error-handling/error-handling) — production-grade patterns for server-side JS
    
*   [Sentry — JavaScript error monitoring](https://docs.sentry.io/platforms/javascript/) — turning caught errors into actionable alerts
