Proper error handling is crucial for building robust applications that can gracefully handle unexpected situations and provide good user experiences.
Errors are unexpected events that occur during program execution that disrupt the normal flow of code. In JavaScript, errors are objects that contain information about what went wrong.
Every error object has two main properties:
message: A human-readable description of the errorstack: A trace of function calls that led to the errorJavaScript has several built-in error types, each serving a specific purpose:
ErrorThe base error class that all other errors inherit from.
const error = new Error("Something went wrong");
console.log(error.message); // "Something went wrong"
TypeErrorOccurs when an operation is performed on a value of the wrong type.
const obj = null;
console.log(obj.property); // TypeError: Cannot read properties of null
ReferenceErrorHappens when trying to access a variable that doesn't exist.
console.log(undefinedVariable); // ReferenceError: undefinedVariable is not defined
SyntaxErrorThrown when there's a syntax mistake in the code.
eval("const x = "); // SyntaxError: Unexpected end of input
RangeErrorOccurs when a numeric value is outside its allowed range.
const arr = new Array(-1); // RangeError: Invalid array length
The try-catch statement allows you to handle errors gracefully without crashing your application.
try {
// Code that might throw an error
const result = riskyOperation();
console.log(result);
} catch (error) {
// Code to handle the error
console.error("An error occurred:", error.message);
}
The finally block always executes, regardless of whether an error occurred or not.
let connection;
try {
connection = openDatabaseConnection();
const data = connection.query("SELECT * FROM users");
return data;
} catch (error) {
console.error("Database error:", error.message);
return [];
} finally {
// This always runs
if (connection) {
connection.close();
}
}
You can nest try-catch blocks for more granular error handling.
try {
try {
const data = JSON.parse(invalidJson);
processData(data);
} catch (parseError) {
console.error("JSON parsing failed:", parseError.message);
// Try with default data
processData(defaultData);
}
} catch (processError) {
console.error("Data processing failed:", processError.message);
}
throw StatementYou can create and throw your own errors using the throw statement.
function validateAge(age) {
if (typeof age !== 'number') {
throw new TypeError("Age must be a number");
}
if (age < 0) {
throw new RangeError("Age cannot be negative");
}
if (age < 18) {
throw new Error("User must be at least 18 years old");
}
return true;
}
try {
validateAge(-5);
} catch (error) {
console.error("Validation error:", error.message);
}
You can extend the Error class to create your own error types.
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = "ValidationError";
this.field = field;
}
}
class NetworkError extends Error {
constructor(message, statusCode) {
super(message);
this.name = "NetworkError";
this.statusCode = statusCode;
}
}
function validateEmail(email) {
if (!email.includes('@')) {
throw new ValidationError("Invalid email format", "email");
}
return true;
}
try {
validateEmail("invalid-email");
} catch (error) {
if (error instanceof ValidationError) {
console.error(`Validation failed for ${error.field}: ${error.message}`);
}
}
When an error is thrown and not caught, it propagates up the call stack until it's caught or reaches the global scope.
function thirdLevel() {
throw new Error("Error in third level");
}
function secondLevel() {
thirdLevel(); // Error propagates here
}
function firstLevel() {
try {
secondLevel(); // Error caught here
} catch (error) {
console.error("Caught in first level:", error.message);
}
}
firstLevel();
Sometimes you want to catch an error, handle it partially, then re-throw it for higher-level handling.
function processData(data) {
try {
if (!data) {
throw new Error("No data provided");
}
// Process data
return processedData;
} catch (error) {
// Log the error
console.error("Processing failed:", error.message);
// Re-throw for higher-level handling
throw error;
}
}
try {
processData(null);
} catch (error) {
console.error("Operation failed:", error.message);
}
You can wrap errors with additional context while preserving the original error.
function apiCall(url) {
try {
// Simulate API call
throw new Error("Network timeout");
} catch (networkError) {
const wrappedError = new Error(`API call to ${url} failed`);
wrappedError.cause = networkError;
wrappedError.originalError = networkError;
throw wrappedError;
}
}
try {
apiCall("https://api.example.com/data");
} catch (error) {
console.error("Main error:", error.message);
console.error("Caused by:", error.cause.message);
}
Promises have built-in error handling using .catch() method.
fetch("https://api.example.com/data")
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => console.log(data))
.catch(error => console.error("Fetch error:", error.message));
With async/await, you can use try-catch blocks for cleaner error handling.
async function fetchUserData(userId) {
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
if (!response.ok) {
throw new Error(`Failed to fetch user: ${response.status}`);
}
const userData = await response.json();
return userData;
} catch (error) {
console.error("Error fetching user data:", error.message);
throw error; // Re-throw for caller to handle
}
}
// Usage
async function displayUser(userId) {
try {
const user = await fetchUserData(userId);
console.log("User:", user.name);
} catch (error) {
console.error("Could not display user:", error.message);
}
}
Handle errors in multiple async operations effectively.
async function fetchMultipleData() {
const results = {
user: null,
posts: null,
comments: null
};
try {
results.user = await fetchUser();
} catch (error) {
console.error("Failed to fetch user:", error.message);
}
try {
results.posts = await fetchPosts();
} catch (error) {
console.error("Failed to fetch posts:", error.message);
}
try {
results.comments = await fetchComments();
} catch (error) {
console.error("Failed to fetch comments:", error.message);
}
return results;
}
Catch specific error types rather than catching everything.
// Good: Specific error handling
try {
const data = JSON.parse(jsonString);
processData(data);
} catch (error) {
if (error instanceof SyntaxError) {
console.error("Invalid JSON format");
} else if (error instanceof ValidationError) {
console.error("Data validation failed");
} else {
console.error("Unexpected error:", error.message);
}
}
// Avoid: Too generic
try {
// some code
} catch (error) {
// This catches everything, even syntax errors in the catch block
}
Error messages should be helpful for debugging.
// Good: Descriptive error messages
function divide(a, b) {
if (b === 0) {
throw new Error(`Cannot divide ${a} by zero`);
}
if (typeof a !== 'number' || typeof b !== 'number') {
throw new TypeError(`Both arguments must be numbers, got ${typeof a} and ${typeof b}`);
}
return a / b;
}
// Avoid: Vague error messages
function divideBad(a, b) {
if (b === 0) {
throw new Error("Error occurred");
}
return a / b;
}
Log errors with sufficient context for debugging.
function processOrder(order) {
try {
validateOrder(order);
saveOrder(order);
sendConfirmation(order);
} catch (error) {
// Log with context
console.error(`Order processing failed for order ${order.id}:`, {
error: error.message,
order: order,
timestamp: new Date().toISOString(),
stack: error.stack
});
// Notify user
showUserMessage("Order processing failed. Please try again.");
// Re-throw if needed
throw error;
}
}
Always handle errors meaningfully, even if it's just logging them.
// Bad: Ignoring errors
try {
riskyOperation();
} catch (error) {
// Empty catch block - error is silently ignored
}
// Good: At least log the error
try {
riskyOperation();
} catch (error) {
console.error("Risky operation failed:", error.message);
// Handle appropriately
}
You can set up global error handlers for uncaught errors.
// Handle synchronous errors
window.addEventListener('error', (event) => {
console.error('Global error handler:', {
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
error: event.error
});
});
// Handle unhandled promise rejections
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled promise rejection:', {
reason: event.reason,
promise: event.promise
});
// Prevent the default browser behavior
event.preventDefault();
});
While this is more React-specific, the concept applies to frameworks.
// Conceptual error boundary pattern
class ErrorBoundary {
constructor() {
this.hasError = false;
this.error = null;
}
catch(error) {
this.hasError = true;
this.error = error;
console.error("Boundary caught error:", error);
}
reset() {
this.hasError = false;
this.error = null;
}
}
The stack trace shows the sequence of function calls that led to the error.
function functionC() {
throw new Error("Error in function C");
}
function functionB() {
functionC();
}
function functionA() {
functionB();
}
try {
functionA();
} catch (error) {
console.log("Stack trace:");
console.log(error.stack);
// Shows the complete call stack
}
Throw errors only in specific conditions for debugging.
function debugLog(message, data) {
if (process.env.NODE_ENV === 'development') {
console.log(`[DEBUG] ${message}:`, data);
}
}
function validateInput(input, strict = false) {
if (strict && !input) {
throw new Error("Input is required in strict mode");
}
debugLog("Input validation", { input, strict });
return true;
}
Implement error reporting for production applications.
function reportError(error, context = {}) {
const errorReport = {
message: error.message,
stack: error.stack,
context: context,
userAgent: navigator.userAgent,
timestamp: new Date().toISOString(),
url: window.location.href
};
// Send to error reporting service
fetch('/api/errors', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(errorReport)
}).catch(reportError => {
console.error("Failed to report error:", reportError);
});
}
// Usage
try {
riskyOperation();
} catch (error) {
reportError(error, { operation: 'riskyOperation', userId: '123' });
}
function validateForm(formData) {
const errors = [];
if (!formData.email) {
errors.push("Email is required");
} else if (!formData.email.includes('@')) {
errors.push("Invalid email format");
}
if (!formData.password) {
errors.push("Password is required");
} else if (formData.password.length < 8) {
errors.push("Password must be at least 8 characters");
}
if (errors.length > 0) {
throw new ValidationError("Form validation failed", errors);
}
return true;
}
// Usage
try {
validateForm({ email: "invalid", password: "123" });
} catch (error) {
if (error instanceof ValidationError) {
error.field.forEach(err => console.error(err));
}
}
class APIError extends Error {
constructor(message, status, data) {
super(message);
this.name = "APIError";
this.status = status;
this.data = data;
}
}
async function apiRequest(url, options = {}) {
try {
const response = await fetch(url, {
headers: {
'Content-Type': 'application/json',
...options.headers
},
...options
});
const data = await response.json();
if (!response.ok) {
throw new APIError(
data.message || `HTTP ${response.status}`,
response.status,
data
);
}
return data;
} catch (error) {
if (error instanceof APIError) {
throw error;
}
throw new APIError("Network error occurred", 0, { originalError: error });
}
}
// Usage
async function loadUserData(userId) {
try {
const user = await apiRequest(`/api/users/${userId}`);
return user;
} catch (error) {
if (error.status === 404) {
console.log("User not found");
return null;
} else if (error.status >= 500) {
console.error("Server error, please try again later");
} else {
console.error("Request failed:", error.message);
}
throw error;
}
}
Error handling is a fundamental skill for writing robust JavaScript applications. Here are the key takeaways:
Remember that good error handling not only prevents crashes but also provides better user experiences and makes debugging easier.