Error Handling
Error Handling in JavaScript
Errors are inevitable in programming. Good error handling makes your applications more robust, user-friendly, and easier to debug.
The Basics: try…catch
The try...catch statement lets you handle errors gracefully:
try {
// Code that might throw an error
const result = riskyOperation();
console.log(result);
} catch (error) {
// Code to handle the error
console.log("Something went wrong:", error.message);
}
Without try…catch:
JSON.parse("invalid json"); // Uncaught SyntaxError - app crashes!
With try…catch:
try {
const data = JSON.parse("invalid json");
console.log(data);
} catch (error) {
console.log("Failed to parse JSON:", error.message);
// App continues running
}
The Error Object
When an error is caught, you receive an Error object with useful properties:
try {
throw new Error("Something went wrong");
} catch (error) {
console.log(error.name); // "Error"
console.log(error.message); // "Something went wrong"
console.log(error.stack); // Stack trace (useful for debugging)
}
The finally Block
Code in finally always runs, whether an error occurred or not:
try {
console.log("Trying...");
riskyOperation();
} catch (error) {
console.log("Error:", error.message);
} finally {
console.log("Cleanup happens here");
// Close connections, release resources, etc.
}
Practical example:
function readFile(filename) {
let file;
try {
file = openFile(filename);
const data = file.read();
return data;
} catch (error) {
console.log("Error reading file:", error.message);
return null;
} finally {
// Always close the file, even if error occurred
if (file) {
file.close();
}
}
}
Throwing Errors
You can throw your own errors using throw:
function divide(a, b) {
if (b === 0) {
throw new Error("Cannot divide by zero");
}
return a / b;
}
try {
const result = divide(10, 0);
} catch (error) {
console.log(error.message); // "Cannot divide by zero"
}
Throw different types:
throw new Error("Generic error");
throw new TypeError("Wrong type provided");
throw new RangeError("Value out of range");
throw new ReferenceError("Variable doesn't exist");
// You can throw any value (but objects are best practice)
throw "Error string"; // Works but not recommended
throw { code: 404, message: "Not found" }; // Also works
Built-in Error Types
JavaScript has several built-in error types:
// TypeError - wrong type
const num = 5;
num.toUpperCase(); // TypeError: num.toUpperCase is not a function
// ReferenceError - variable doesn't exist
console.log(nonExistentVariable); // ReferenceError
// RangeError - value out of range
const arr = new Array(-1); // RangeError: Invalid array length
// SyntaxError - invalid syntax (usually at parse time)
eval("var 123abc = 5"); // SyntaxError: Invalid variable name
Custom Errors
Create custom error classes for specific situations:
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
}
class DatabaseError extends Error {
constructor(message, query) {
super(message);
this.name = "DatabaseError";
this.query = query;
}
}
// Usage
function validateUser(user) {
if (!user.email) {
throw new ValidationError("Email is required");
}
if (user.age < 0) {
throw new ValidationError("Age cannot be negative");
}
}
try {
validateUser({ name: "Alice" });
} catch (error) {
if (error instanceof ValidationError) {
console.log("Validation failed:", error.message);
} else {
console.log("Unknown error:", error);
}
}
Handling Different Error Types
try {
someOperation();
} catch (error) {
if (error instanceof ValidationError) {
// Show user-friendly message
displayError(error.message);
} else if (error instanceof DatabaseError) {
// Log for debugging, show generic message to user
console.error("DB Error:", error.query);
displayError("Database error occurred");
} else {
// Unexpected error
console.error("Unexpected error:", error);
displayError("Something went wrong");
}
}
Async Error Handling
With Promises:
// Using .catch()
fetch("https://api.example.com/data")
.then(response => response.json())
.then(data => console.log(data))
.catch(error => {
console.log("Fetch failed:", error.message);
});
// With try...catch (needs async function)
async function fetchData() {
try {
const response = await fetch("https://api.example.com/data");
const data = await response.json();
console.log(data);
} catch (error) {
console.log("Fetch failed:", error.message);
}
}
Handling multiple async operations:
async function loadUserData(userId) {
try {
const user = await fetchUser(userId);
const posts = await fetchUserPosts(userId);
const comments = await fetchUserComments(userId);
return { user, posts, comments };
} catch (error) {
console.log("Failed to load user data:", error.message);
// Return partial data or default values
return { user: null, posts: [], comments: [] };
}
}
Practical Examples
Example 1: Form Validation
function validateForm(formData) {
const errors = [];
try {
if (!formData.username || formData.username.length < 3) {
throw new ValidationError("Username must be at least 3 characters");
}
if (!formData.email || !formData.email.includes("@")) {
throw new ValidationError("Valid email is required");
}
if (!formData.password || formData.password.length < 8) {
throw new ValidationError("Password must be at least 8 characters");
}
return { valid: true, errors: [] };
} catch (error) {
return { valid: false, errors: [error.message] };
}
}
// Usage
const result = validateForm({ username: "Al", email: "invalid" });
if (!result.valid) {
console.log("Validation errors:", result.errors);
}
Example 2: API Wrapper with Error Handling
class APIClient {
async request(endpoint, options = {}) {
try {
const response = await fetch(endpoint, options);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
return { success: true, data };
} catch (error) {
console.error("API request failed:", error);
return {
success: false,
error: error.message,
// Don't expose stack trace to user
userMessage: "Unable to connect to server. Please try again."
};
}
}
}
// Usage
const api = new APIClient();
const result = await api.request("/api/users");
if (result.success) {
console.log("Users:", result.data);
} else {
alert(result.userMessage);
}
Example 3: Retry Logic
async function retryOperation(operation, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
if (attempt === maxRetries) {
throw new Error(`Failed after ${maxRetries} attempts: ${error.message}`);
}
console.log(`Attempt ${attempt} failed, retrying...`);
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
}
}
}
// Usage
try {
const data = await retryOperation(() => fetch("/api/unstable-endpoint"));
console.log("Success:", data);
} catch (error) {
console.log("All retries failed:", error.message);
}
Example 4: User-Friendly Error Messages
function getUserFriendlyError(error) {
const errorMap = {
"NetworkError": "Please check your internet connection",
"TimeoutError": "Request took too long. Please try again",
"AuthError": "Please log in to continue",
"ValidationError": error.message, // Use the specific validation message
"NotFoundError": "The requested item was not found"
};
return errorMap[error.name] || "An unexpected error occurred";
}
// Usage
try {
await saveUserProfile(userData);
} catch (error) {
const message = getUserFriendlyError(error);
showToast(message, "error");
}
Best Practices
1. Don’t catch errors you can’t handle:
// Bad - catch and ignore
try {
criticalOperation();
} catch (error) {
// Ignoring error
}
// Good - only catch if you can handle it
try {
criticalOperation();
} catch (error) {
logError(error);
notifyUser("Operation failed");
// Take corrective action
}
2. Provide context in error messages:
// Bad
throw new Error("Invalid value");
// Good
throw new Error(`Invalid value for ${fieldName}: expected ${expected}, got ${actual}`);
3. Log errors properly:
catch (error) {
// Log full error for debugging
console.error("Error details:", {
message: error.message,
stack: error.stack,
timestamp: new Date(),
userId: currentUser?.id
});
// Show user-friendly message
displayError("Something went wrong. Please try again.");
}
4. Clean up resources:
let connection;
try {
connection = await database.connect();
await connection.query(sql);
} catch (error) {
console.error("Database error:", error);
} finally {
// Always close connection
if (connection) {
await connection.close();
}
}
5. Validate early:
function processPayment(amount, account) {
// Validate at the start
if (typeof amount !== "number" || amount <= 0) {
throw new ValidationError("Amount must be a positive number");
}
if (!account || !account.id) {
throw new ValidationError("Valid account is required");
}
// Continue with processing...
}
Common Patterns
Error boundaries (conceptually):
function safeExecute(fn, fallback) {
try {
return fn();
} catch (error) {
console.error(error);
return fallback;
}
}
const result = safeExecute(
() => JSON.parse(userInput),
{} // Fallback to empty object
);
Global error handler:
window.addEventListener("error", (event) => {
console.error("Global error:", event.error);
// Send to error tracking service
trackError(event.error);
});
// For unhandled promise rejections
window.addEventListener("unhandledrejection", (event) => {
console.error("Unhandled promise rejection:", event.reason);
trackError(event.reason);
});
Next Article: Regular Expressions I