The Ultimate Guide to Error Handling in JavaScript: try-catch, Throwing, and Real-World Practices

Sadanand gadwalSadanand gadwal
5 min read

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

  • try...catch blocks — to catch and handle exceptions
  • throw — to create and raise your own errors when something is invalid
  • Patterns to handle async errors, because JavaScript is heavily asynchronous

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

  • Why JavaScript errors happen
  • How try...catch really works under the hood
  • The purpose of throw and when to use it
  • How to handle errors in promises and async/await
  • Real-world design patterns: input validation, fallback values, logging, and user feedback

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:

  • Code inside the try block runs normally.
  • If an error is thrown inside try, JavaScript stops running the rest of try immediately.
  • Control jumps to the catch block.
  • The catch block gets the error object with information about what went wrong.

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, 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:

  • Execution immediately stops at the throw.
  • Control looks for the nearest catch block up the call stack.
  • If no catch exists, the program crashes.

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

  • Never ignore errors silently.
  • Add meaningful, precise messages.
  • Use custom error classes for clarity if needed.
  • Catch only what you can handle. Don’t catch and swallow everything.
  • For async operations, always .catch() promises or use try-catch with await.
  • Always log unexpected errors somewhere.

9. Code Summary

ConceptPurposeExample
try...catchRun risky code and handle errorstry { risky() } catch (err) { handle(err) }
throwCreate your own error for invalid statesthrow new Error("Invalid input")
finallyAlways runs, useful for cleanupfinally { cleanup() }
Async errorsUse .catch for promisesfetch().then().catch()
Async/awaitWrap with try...catchtry { await risky() } catch (err) {}

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 user’s 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.

5
Subscribe to my newsletter

Read articles from Sadanand gadwal directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Sadanand gadwal
Sadanand gadwal

I am a passionate Full Stack Web Developer specializing in the MERN stack (MongoDB, Express.js, React.js, and Node.js). I have completed my master's in MCA from the Central University of Karnataka, Kalaburagi. With a strong focus on web development, I possess expertise in technologies such as React.js, tailwind CSS, Material UI, etc.