Error Handling in JavaScript
In any software application, things will inevitably go wrong. Networks fail, APIs return unexpected data, and bugs occur. Error Handling is the practice of anticipating, detecting, and gracefully recovering from these exceptional conditions to prevent the application from crashing unexpectedly.
1. The try...catch...finally Statement
The primary mechanism for handling runtime errors in JavaScript is the try...catch block.
Basic Syntax
try {
// Code that may throw an error
const data = JSON.parse("{ malformed JSON }");
console.log("This line will not run if the line above fails");
} catch (error) {
// Code to handle the error
console.error("An error occurred while parsing JSON:", error.message);
}
tryblock: You put the "risky" code here. If an error occurs, execution immediately jumps to thecatchblock.catchblock: Receives the error object. It allows you to log the error, display a fallback UI, or attempt a recovery.
The finally Block
The finally block is optional but very useful. Code inside finally always executes, regardless of whether an error was thrown or successfully caught. It is typically used for cleanup operations (like closing loaders, database connections, or file streams).
let isLoading = true;
try {
// Fetch data
riskyOperation();
} catch (err) {
console.error("Failed to load:", err);
} finally {
// This will always run
isLoading = false;
console.log("Operation finished.");
}
2. Throwing Errors Manually
Sometimes you want to intentionally generate an error when an invalid condition is met (e.g., bad arguments passed to a function). You use the throw keyword for this.
function divide(a, b) {
if (b === 0) {
throw new Error("Division by zero is not allowed.");
}
return a / b;
}
try {
divide(10, 0);
} catch (e) {
console.error(e.message); // "Division by zero is not allowed."
}
When throw is executed, the function stops immediately, and control is passed to the nearest catch block in the call stack.
3. Built-in Error Types
While new Error() is the most common generic error, JavaScript has several native error constructors that provide more specific semantics:
TypeError: Thrown when a value is not of the expected type (e.g., trying to call a string as a function,const x = "hi"; x();).ReferenceError: Thrown when trying to access a variable that has not been declared.SyntaxError: Thrown by the JavaScript engine when parsing syntactically invalid code (e.g., missing a bracket).RangeError: Thrown when a number is outside an allowable range (e.g.,new Array(-1)).
4. Custom Error Classes
For large applications, throwing generic Error objects isn't enough. You can extend the base Error class to create custom, domain-specific errors. This makes debugging and error propagation much cleaner.
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
this.statusCode = 400; // Custom property
}
}
function validateUser(user) {
if (!user.age || user.age < 18) {
throw new ValidationError("User must be 18 or older.");
}
}
try {
validateUser({ name: "Timmy", age: 12 });
} catch (err) {
if (err instanceof ValidationError) {
console.warn(`Validation Failed [${err.statusCode}]: ${err.message}`);
} else {
// Handle other unexpected errors
console.error("An unexpected error occurred:", err);
}
}
5. Asynchronous Error Handling
Promises (.catch())
When working with Promises, errors are caught using the .catch() method rather than a try...catch block.
fetch('https://api.example.com/data')
.then(response => {
if (!response.ok) throw new Error("HTTP Error");
return response.json();
})
.then(data => console.log(data))
.catch(error => console.error("Fetch failed:", error.message));
async / await
With modern async/await syntax, you can return to using the standard try...catch blocks, which makes asynchronous code look and behave like synchronous code.
async function getUserData() {
try {
const response = await fetch('https://api.example.com/user/1');
if (!response.ok) throw new Error("User not found");
const data = await response.json();
return data;
} catch (error) {
console.error("Error fetching user:", error);
// You can also re-throw the error to be handled by the caller
// throw error;
}
}
Summary
Effective error handling prevents silent failures and catastrophic crashes. By combining try/catch/finally, custom error classes, and robust async handling, you ensure your application provides a stable, predictable experience for both users and fellow developers.