Errors are inevitable. Good code doesn’t just work when everything goes right — it stays predictable and safe when things go wrong.

In JavaScript, error handling is mainly done with three tools:

This article goes beyond the basics. We’ll cover:

1. What is an error in JavaScript?

When the JavaScript engine encounters a problem it cannot resolve — like trying to access an undefined variable, calling a function that does not exist, or failing to parse data — it throws an error. If this error is not handled, it bubbles up and can crash your script.

// Uncaught ReferenceError
console.log(user.name);
// ReferenceError: user is not defined

The program stops here if you don’t catch this problem.

2. The Try-Catch Mechanism

Purpose: try...catch lets you safely run code that might fail, and handle the failure in a controlled way.

How it works:

Basic syntax:

try {
  // code that might fail
} catch (error) {
  // code to handle the failure
}

If no error occurs in try, the catch block is skipped entirely.

Example: Parsing JSON that might be malformed

try {
  const jsonString = '{"name":"Alice"}';
  const user = JSON.parse(jsonString);
  console.log(user.name); // Alice

  // This JSON is invalid: missing quote
  const badJson = '{"name": Alice}';
  JSON.parse(badJson);
} catch (err) {
  console.error("JSON parsing failed:", err.message);
}

Use try...catch only around risky operations: user input parsing, network requests, and file operations.

3. The throw Statement — Creating Your Own Errors

Sometimes, your program detects a problem that the JavaScript engine itself would not consider an error. For example, maybe a number is negative when it should not be.

To handle this, you can throw your own errors.

Basic syntax:

throw new Error("Something went wrong");

When you throw:

Example: Validating a function argument

function calculateArea(radius) {
  if (radius <= 0) {
    throw new Error("Radius must be positive");
  }
  return Math.PI * radius * radius;
}

try {
  console.log(calculateArea(5)); // Works fine
  console.log(calculateArea(-2)); // Throws
} catch (err) {
  console.error("Calculation failed:", err.message);
}

Use throw when you hit a state that should never happen in correct usage. It enforces contracts: "This function must not get bad input."

4. The finally Block — Guaranteed Cleanup

finally always runs, whether the code in try succeeds, fails, or even if you return from try.

Example:

try {
  console.log("Opening connection");
  throw new Error("Connection failed");
} catch (err) {
  console.error("Error:", err.message);
} finally {
  console.log("Closing connection");
}

// Output:
// Opening connection
// Error: Connection failed
// Closing connection

Use finally for closing files or database connections, stopping loaders/spinners, or resetting states.

5. Asynchronous Code: The Common Trap

JavaScript runs lots of code asynchronously — setTimeout, fetch, promises. try...catch does not automatically catch errors that happen inside callbacks or promises.

Example:

try {
  setTimeout(() => {
    throw new Error("Oops");
  }, 1000);
} catch (err) {
  console.log("Caught:", err.message); // Never runs
}

How to Handle Async Errors Properly

Wrap inside the async function

setTimeout(() => {
  try {
    throw new Error("Oops inside timeout");
  } catch (err) {
    console.error("Caught:", err.message);
  }
}, 1000);

Promises: use .catch

fetch("https://bad.url")
  .then(res => res.json())
  .catch(err => console.error("Network or parsing failed:", err.message));

Async/await: wrap with try-catch

async function fetchUser() {
  try {
    const response = await fetch("https://bad.url");
    const data = await response.json();
    console.log(data);
  } catch (err) {
    console.error("Async/await failed:", err.message);
  }
}
fetchUser();

6. Real-World Example: Form Validation

Putting it together with a user registration check.

function registerUser(user) {
  if (!user.username) {
    throw new Error("Username is required");
  }
  if (user.password.length < 8) {
    throw new Error("Password must be at least 8 characters");
  }

  return "User registered!";
}

try {
  const user = { username: "John", password: "123" };
  const result = registerUser(user);
  console.log(result);
} catch (err) {
  console.error("Registration failed:", err.message);
}

7. Logging and Rethrowing

Catch an error just to log it, then rethrow so a higher-level handler can deal with it.

function processData(data) {
  try {
    if (!data) {
      throw new Error("No data");
    }
    // process...
  } catch (err) {
    console.error("Log:", err.message);
    throw err; // propagate further
  }
}

try {
  processData(null);
} catch (err) {
  console.log("Final catch:", err.message);
}

8. Best Practices

Conclusion

Bad things happen in software — it’s how you prepare for them that separates a robust application from a fragile one. Handle predictable failures, fail loudly on developer errors, and never let unexpected problems silently break your users’ trust.

If you understand how try...catch, throw, and async error handling fit together, you have a safety net for whatever the real world throws at your code.