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:
-
try...catchblocks — 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...catchreally works under the hood - The purpose of
throwand 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
tryblock runs normally. - If an error is thrown inside
try, JavaScript stops running the rest oftryimmediately. - Control jumps to the
catchblock. - The
catchblock 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
catchblock up the call stack. - If no
catchexists, 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-catchwithawait. - 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.
Top comments (0)