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

Table of contents

In JavaScript, error handling is mainly done with three tools:
try...catch
blocks — to catch and handle exceptionsthrow
— 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 oftry
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 usetry-catch
withawait
. - Always log unexpected errors somewhere.
9. Code Summary
Concept | Purpose | Example |
try...catch | Run risky code and handle errors | try { risky() } catch (err) { handle(err) } |
throw | Create your own error for invalid states | throw new Error("Invalid input") |
finally | Always runs, useful for cleanup | finally { cleanup() } |
Async errors | Use .catch for promises | fetch().then().catch() |
Async/await | Wrap with try...catch | try { 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.
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.