Error Handling in JavaScript: Try, Catch, Finally Explained
Introduction
Imagine you're using a mobile app and something goes wrong. Instead of the app crashing and closing unexpectedly, it shows a helpful error message and lets you continue using it. That's good error handling. In JavaScript, errors are inevitable. Whether it's a user entering invalid data, a network request failing, or a bug in your code, knowing how to handle errors gracefully is essential for building reliable applications. In this blog, we'll explore what errors are, how to catch and handle them properly, and why error handling is so important for professional development.
1. What Are Errors in JavaScript?
Errors are exceptional conditions that occur during program execution. They can be caused by mistakes in code, unexpected user input, or external factors like network failures.
Types of JavaScript Errors:
// 1. Syntax Error - Invalid code structure (caught at parse time)
// var x = ; // SyntaxError: Unexpected token
// 2. ReferenceError - Using undefined variable
console.log(undefinedVariable); // ReferenceError: undefinedVariable is not defined
// 3. TypeError - Using a value incorrectly
const str = "hello";
str.toUpperCase.toUpperCase(); // TypeError: str.toUpperCase.toUpperCase is not a function
// 4. RangeError - Invalid numeric value
const arr = new Array(-1); // RangeError: Invalid array length
// 5. Error - Generic error
throw new Error("Something went wrong!");
How Errors Occur in Real Situations:
// Example 1: Accessing property of undefined
const user = null;
console.log(user.name); // TypeError: Cannot read property 'name' of null
// Example 2: Calling undefined function
const getData = undefined;
getData(); // TypeError: getData is not a function
// Example 3: Invalid operation
const num = "5";
const result = num.map(x => x); // TypeError: num.map is not a function
// Example 4: Division by zero (not an error, returns Infinity)
console.log(10 / 0); // Infinity (No error!)
// Example 5: JSON parsing invalid JSON
const json = "{invalid json}";
JSON.parse(json); // SyntaxError: Unexpected token
Error Object Structure:
try {
undefinedFunc();
} catch (error) {
console.log(error.name); // "ReferenceError"
console.log(error.message); // "undefinedFunc is not defined"
console.log(error.stack); // Full stack trace for debugging
}
2. Using Try and Catch Blocks
The try and catch blocks are the primary way to handle errors in JavaScript. try contains code that might throw an error, and catch handles it if it does.
Basic Structure:
try {
// Code that might cause an error
riskyOperation();
} catch (error) {
// Code to handle the error
console.log("An error occurred:", error.message);
}
Simple Example:
try {
const result = JSON.parse("invalid json");
} catch (error) {
console.log("Failed to parse JSON:", error.message);
// Program continues instead of crashing
}
console.log("Program continues..."); // This still executes!
Catching Specific Error Types:
// Without try-catch - program crashes here
const user = null;
console.log(user.name); // TypeError: Cannot read property 'name' of null
console.log("This never executes"); // Never reached
// ----
// With try-catch - handle gracefully
try {
const user = null;
console.log(user.name);
} catch (error) {
console.log("Caught an error:", error.message);
}
console.log("Program continues"); // This executes!
Checking Error Type:
try {
const data = JSON.parse("{invalid}");
} catch (error) {
if (error instanceof SyntaxError) {
console.log("Invalid JSON format");
} else if (error instanceof TypeError) {
console.log("Type mismatch");
} else {
console.log("Unknown error:", error.message);
}
}
Real-World Example: API Call
async function fetchUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP Error: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.log("Failed to fetch user:", error.message);
return null; // Return default value
}
}
// Usage
const user = await fetchUserData(1);
if (user) {
console.log(user);
} else {
console.log("Could not load user data");
}
Try-Catch with Different Operations:
try {
// Operation 1: Accessing property
const user = { name: "Raj" };
console.log(user.name); // "Raj" - OK
// Operation 2: Array access
const arr = [1, 2, 3];
console.log(arr[0]); // 1 - OK
// Operation 3: Function call
const result = parseInt("123");
console.log(result); // 123 - OK
// Operation 4: File operations (would fail if file doesn't exist)
// const content = fs.readFileSync("/path/to/file.txt");
} catch (error) {
console.log("Error occurred:", error.message);
}
Multiple Catches (Not Standard, But Useful Pattern):
// JavaScript doesn't support multiple catch blocks natively
// But you can use if-else inside catch
try {
// Some risky operation
throw new TypeError("Invalid type");
} catch (error) {
if (error instanceof TypeError) {
console.log("Type Error:", error.message);
} else if (error instanceof ReferenceError) {
console.log("Reference Error:", error.message);
} else {
console.log("Other Error:", error.message);
}
}
Rethrowing Errors:
// Sometimes you want to handle an error partially and then rethrow it
try {
riskyOperation();
} catch (error) {
console.log("Logging error for debugging:", error.message);
// Log to external service
logErrorToServer(error);
// Rethrow for caller to handle
throw error;
}
3. The Finally Block
The finally block executes regardless of whether an error occurs. It's perfect for cleanup operations.
Basic Structure:
try {
// Code that might throw an error
riskyOperation();
} catch (error) {
// Handle the error
console.log("Error:", error.message);
} finally {
// This ALWAYS runs
console.log("Cleanup code");
}
Execution Order:
console.log("1. Start");
try {
console.log("2. In try block");
throw new Error("Something went wrong");
console.log("3. This won't execute");
} catch (error) {
console.log("4. In catch block:", error.message);
} finally {
console.log("5. In finally block");
}
console.log("6. After try-catch-finally");
// Output:
// 1. Start
// 2. In try block
// 4. In catch block: Something went wrong
// 5. In finally block
// 6. After try-catch-finally
Finally Always Executes (Even with Return):
function testFinally() {
try {
return "from try";
} finally {
console.log("finally executes!"); // This executes even with return!
}
}
const result = testFinally();
console.log(result); // "from try"
// Output:
// finally executes!
// from try
Practical Use Cases for Finally:
Use Case 1: Closing Resources
function readFile(filename) {
let file = null;
try {
file = openFile(filename); // Open file
const content = file.read(); // Read content
return content;
} catch (error) {
console.log("Error reading file:", error.message);
return null;
} finally {
if (file) {
file.close(); // Always close the file
}
}
}
Use Case 2: Cleaning Up State
let isLoading = false;
async function fetchData(url) {
isLoading = true;
try {
const response = await fetch(url);
return await response.json();
} catch (error) {
console.log("Fetch failed:", error.message);
return null;
} finally {
isLoading = false; // Always update loading state
}
}
Use Case 3: Hiding Loading Indicator
function showLoadingSpinner() {
document.getElementById("spinner").style.display = "block";
}
function hideLoadingSpinner() {
document.getElementById("spinner").style.display = "none";
}
async function loadUserData(userId) {
showLoadingSpinner();
try {
const response = await fetch(`/api/users/${userId}`);
return await response.json();
} catch (error) {
console.log("Failed to load user:", error.message);
return null;
} finally {
hideLoadingSpinner(); // Always hide, even on error
}
}
Use Case 4: Database Connection
async function queryDatabase(query) {
let connection = null;
try {
connection = await db.connect();
const results = await connection.query(query);
return results;
} catch (error) {
console.log("Query failed:", error.message);
return [];
} finally {
if (connection) {
connection.close(); // Always close connection
}
}
}
Finally with No Catch:
// You can have try-finally without catch
try {
const data = JSON.parse(jsonString);
processData(data);
} finally {
console.log("Cleanup regardless of errors");
// If error occurs, it will propagate after finally
}
4. Throwing Custom Errors
Sometimes you need to throw your own errors to signal that something went wrong in your application logic.
Throwing Simple Errors:
// Throw a string (not recommended)
throw "Something went wrong";
// Throw an Error object (recommended)
throw new Error("Something went wrong");
Creating Custom Error Classes:
// Custom error class
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
}
// Using custom error
function validateEmail(email) {
if (!email.includes("@")) {
throw new ValidationError("Invalid email format");
}
return true;
}
try {
validateEmail("invalid-email");
} catch (error) {
if (error instanceof ValidationError) {
console.log("Validation failed:", error.message);
}
}
Practical Example: User Validation
class ValidationError extends Error {
constructor(field, message) {
super(`\({field}: \){message}`);
this.name = "ValidationError";
this.field = field;
}
}
function validateUser(user) {
if (!user.name || user.name.trim() === "") {
throw new ValidationError("name", "Name is required");
}
if (!user.email || !user.email.includes("@")) {
throw new ValidationError("email", "Valid email is required");
}
if (!user.age || user.age < 18) {
throw new ValidationError("age", "Must be 18 or older");
}
return true;
}
try {
validateUser({ name: "Ashish", email: "invalid", age: 21 });
} catch (error) {
if (error instanceof ValidationError) {
console.log(`\({error.field}: \){error.message}`);
}
}
Throwing Errors Based on Conditions:
function divide(a, b) {
if (typeof a !== "number" || typeof b !== "number") {
throw new TypeError("Both arguments must be numbers");
}
if (b === 0) {
throw new Error("Cannot divide by zero");
}
return a / b;
}
try {
console.log(divide(10, 2)); // 5
console.log(divide(10, 0)); // Error!
} catch (error) {
console.log("Calculation error:", error.message);
}
Multiple Custom Errors:
class AuthenticationError extends Error {
constructor(message) {
super(message);
this.name = "AuthenticationError";
}
}
class AuthorizationError extends Error {
constructor(message) {
super(message);
this.name = "AuthorizationError";
}
}
function login(username, password) {
if (!username || !password) {
throw new AuthenticationError("Username and password required");
}
// Simulate user lookup
if (username !== "admin") {
throw new AuthenticationError("Invalid credentials");
}
return { username, token: "abc123" };
}
function accessAdminPanel(user) {
if (!user.isAdmin) {
throw new AuthorizationError("Admin access required");
}
}
try {
const user = login("admin", "password");
accessAdminPanel(user);
} catch (error) {
if (error instanceof AuthenticationError) {
console.log("Auth Error:", error.message);
} else if (error instanceof AuthorizationError) {
console.log("Access Denied:", error.message);
} else {
console.log("Unknown error:", error.message);
}
}
Throwing with Stack Trace Preservation:
function processData(data) {
try {
const parsed = JSON.parse(data);
if (!parsed.id) {
throw new Error("ID field is required");
}
return parsed;
} catch (error) {
// Add context while preserving stack trace
console.error("Failed to process data:", error);
// Rethrow with additional context
throw error; // Stack trace is preserved
}
}
5. Why Error Handling Matters
Proper error handling is critical for professional, reliable applications. Let's understand why.
Reason 1: Prevents Application Crashes
// Without error handling - app crashes
const userInput = "not a number";
const number = parseInt(userInput);
const result = number * 2; // If something goes wrong, app stops here
console.log("Result:", result); // Never executes on error
// ----
// With error handling - app continues
try {
const userInput = "not a number";
const number = parseInt(userInput);
if (isNaN(number)) {
throw new Error("Invalid number");
}
const result = number * 2;
console.log("Result:", result);
} catch (error) {
console.log("Error:", error.message);
console.log("Using default value"); // Still continues
}
Reason 2: Better User Experience
// Without error handling - user sees blank page
async function loadUserProfile(userId) {
const response = await fetch(`/api/users/${userId}`);
const user = await response.json(); // If fails, crashes
displayUserProfile(user);
}
// ----
// With error handling - user sees message
async function loadUserProfile(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error("Failed to load profile");
}
const user = await response.json();
displayUserProfile(user);
} catch (error) {
showErrorMessage("Could not load profile. Please try again later.");
}
}
Reason 3: Easier Debugging
// Without error handling - hard to debug
function complexOperation() {
const result1 = operation1(); // Which failed?
const result2 = operation2(result1); // This?
const result3 = operation3(result2); // Or this?
return result3;
}
// ----
// With error handling - clear where it failed
function complexOperation() {
try {
const result1 = operation1();
console.log("Operation 1 succeeded");
const result2 = operation2(result1);
console.log("Operation 2 succeeded");
const result3 = operation3(result2);
console.log("Operation 3 succeeded");
return result3;
} catch (error) {
console.error("Operation failed:", error.message);
console.error("Stack trace:", error.stack);
}
}
Reason 4: Resource Cleanup
// Without finally - resources might leak
async function downloadFile(url) {
const file = await openFile();
const content = await fetch(url); // What if this fails?
await file.write(content);
file.close(); // Never called if fetch fails!
}
// ----
// With finally - always cleanup
async function downloadFile(url) {
let file = null;
try {
file = await openFile();
const content = await fetch(url);
await file.write(content);
} catch (error) {
console.log("Download failed:", error.message);
} finally {
if (file) {
file.close(); // Always closes
}
}
}
Reason 5: Production Monitoring
// Log errors for monitoring in production
async function criticalOperation() {
try {
// Important business logic
return await processSensitiveData();
} catch (error) {
// Log for monitoring
console.error("Critical operation failed:", error);
// Send to error tracking service
sendErrorToSentry({
message: error.message,
stack: error.stack,
timestamp: new Date()
});
// Notify admin
notifyAdminOfError(error);
// Return safe default
return { success: false };
}
}
Reason 6: Validation and Data Integrity
// Prevent invalid data from being processed
function saveUserData(userData) {
try {
validateUser(userData);
// Only save if validation passes
database.save(userData);
return { success: true };
} catch (error) {
return {
success: false,
error: error.message
};
}
}
function validateUser(user) {
if (!user.email || !user.email.includes("@")) {
throw new Error("Invalid email");
}
if (!user.age || user.age < 0 || user.age > 150) {
throw new Error("Invalid age");
}
}
6. Error Handling Patterns
Pattern 1: Try-Catch-Finally
function fetchData(url) {
try {
// Attempt operation
const response = fetch(url);
return response;
} catch (error) {
// Handle error
console.log("Fetch failed:", error.message);
return null;
} finally {
// Cleanup
console.log("Fetch attempt completed");
}
}
Pattern 2: Error Wrapping
// Wrap low-level errors in meaningful ones
function getUserFromDatabase(userId) {
try {
return db.query(`SELECT * FROM users WHERE id = ${userId}`);
} catch (error) {
throw new Error(`Failed to load user \({userId}: \){error.message}`);
}
}
Pattern 3: Graceful Degradation
// Continue with default values on error
function getUser(userId) {
try {
return fetch(`/api/users/${userId}`).then(r => r.json());
} catch (error) {
console.log("Using cached user data");
return getCachedUser(userId); // Fallback
}
}
Pattern 4: Silent Failure (Use Carefully)
// Sometimes you want to fail silently
function tryParse(json) {
try {
return JSON.parse(json);
} catch (error) {
// Don't throw, just return null
return null;
}
}
Pattern 5: Error Logging
// Log errors with context
function logError(error, context) {
const errorInfo = {
message: error.message,
stack: error.stack,
context: context,
timestamp: new Date(),
userAgent: navigator.userAgent
};
// Send to server
fetch("/api/errors", {
method: "POST",
body: JSON.stringify(errorInfo)
});
}
try {
riskyOperation();
} catch (error) {
logError(error, { operation: "fetchUserData", userId: 123 });
}
7. Common Error Handling Mistakes
Mistake 1: Silent Failures
// Bad: Silently failing
try {
processData();
} catch (error) {
// Empty - error is swallowed
}
// Good: Handle or log
try {
processData();
} catch (error) {
console.error("Processing failed:", error);
throw error; // Rethrow if critical
}
Mistake 2: Catching Everything Without Discrimination
// Bad: Catches all errors
try {
const data = JSON.parse(json);
const result = riskyOperation(data);
return result;
} catch (error) {
// Can't tell what failed
return null;
}
// Good: Be specific
try {
const data = JSON.parse(json);
return riskyOperation(data);
} catch (error) {
if (error instanceof SyntaxError) {
console.log("Invalid JSON");
} else if (error instanceof TypeError) {
console.log("Type error in operation");
} else {
throw error; // Unknown error, rethrow
}
}
Mistake 3: Not Cleaning Up Resources
// Bad: Resource leak
async function download(url) {
const file = openFile();
const data = await fetch(url); // If this fails...
file.write(data);
file.close(); // Never called!
}
// Good: Always cleanup
async function download(url) {
const file = openFile();
try {
const data = await fetch(url);
file.write(data);
} finally {
file.close(); // Always called
}
}
Mistake 4: Losing Original Error Information
// Bad: Losing stack trace
try {
riskyOperation();
} catch (error) {
throw new Error("Failed"); // Stack trace lost
}
// Good: Preserve original error
try {
riskyOperation();
} catch (error) {
throw new Error(`Failed: ${error.message}`);
// Or just rethrow
throw error;
}
Mistake 5: Catching Operational Errors as Bugs
// Bad: Treating user input error as code bug
try {
const age = parseInt(userInput);
if (age < 0) throw new Error("Invalid age");
processAge(age);
} catch (error) {
// This treats validation error same as code error
reportBug(error);
}
// Good: Handle validation separately
function validateAge(age) {
if (age < 0) return false;
return true;
}
try {
const age = parseInt(userInput);
if (!validateAge(age)) {
showError("Please enter a valid age");
return;
}
processAge(age);
} catch (error) {
// Only actual code errors reach here
reportBug(error);
}
8. Error Handling Flow Diagram
Code Execution
|
v
┌──────────────┐
│ try block │
│ │
│ Normal code │
└──────────────┘
|
┌──────────┴──────────┐
| |
No Error Thrown Error Thrown
| |
| v
| ┌──────────────────┐
| │ catch block │
| │ │
| │ Error handling │
| └──────────────────┘
| |
├─────────────────────┤
| |
v v
┌──────────────┐ ┌──────────────┐
│ finally block│ │ finally block│
│ (runs) │ │ (runs) │
└──────────────┘ └──────────────┘
| |
v v
Continue execution Continue or Rethrow
9. Real-World Error Handling Example
// Complete example combining all concepts
class APIError extends Error {
constructor(status, message) {
super(message);
this.name = "APIError";
this.status = status;
}
}
class ValidationError extends Error {
constructor(field, message) {
super(message);
this.name = "ValidationError";
this.field = field;
}
}
async function registerUser(userData) {
let response = null;
try {
// Validate input
validateUserData(userData);
console.log("✓ Validation passed");
// Make API call
response = await fetch("/api/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(userData)
});
console.log("✓ API call completed");
// Check response
if (!response.ok) {
throw new APIError(response.status, "Registration failed");
}
const result = await response.json();
console.log("✓ User registered successfully");
return result;
} catch (error) {
// Handle different error types
if (error instanceof ValidationError) {
console.error(`Validation Error - \({error.field}: \){error.message}`);
return { success: false, error: error.message, field: error.field };
} else if (error instanceof APIError) {
console.error(`API Error \({error.status}: \){error.message}`);
return { success: false, error: error.message };
} else {
console.error("Unexpected error:", error.message);
return { success: false, error: "An unexpected error occurred" };
}
} finally {
console.log("✓ Registration attempt completed");
}
}
function validateUserData(data) {
if (!data.email || !data.email.includes("@")) {
throw new ValidationError("email", "Valid email required");
}
if (!data.password || data.password.length < 8) {
throw new ValidationError("password", "Password must be at least 8 characters");
}
if (!data.name || data.name.trim() === "") {
throw new ValidationError("name", "Name is required");
}
}
// Usage
await registerUser({
name: "Ashish",
email: "ashish@example.com",
password: "securePass123"
});
10. Best Practices for Error Handling
Best Practice 1: Be Specific
// Bad: Too generic
try {
doSomething();
} catch (e) {
console.log("Error");
}
// Good: Specific and informative
try {
doSomething();
} catch (error) {
console.error(`Failed to do something: ${error.message}`);
console.error(`Stack: ${error.stack}`);
}
Best Practice 2: Use Custom Errors
// Bad: Generic errors
throw new Error("Invalid");
// Good: Specific error classes
throw new ValidationError("email", "Invalid email format");
Best Practice 3: Always Cleanup
// Always use finally for cleanup
try {
operation();
} finally {
cleanup();
}
Best Practice 4: Log Errors in Production
try {
operation();
} catch (error) {
// Log for debugging
console.error(error);
// Send to monitoring service
logToMonitoringService(error);
}
Best Practice 5: Provide Meaningful Messages
// Bad
throw new Error("Failed");
// Good
throw new Error(
`Failed to save user \({userId} to database: \){error.message}`
);
Conclusion
Error handling is essential for professional JavaScript development. Here's what we've learned:
Key Concepts:
Errors are exceptional conditions during execution
Try-Catch blocks catch and handle errors
Finally always runs, perfect for cleanup
Custom Errors make error handling clear
Error Handling prevents crashes and improves UX
Why It Matters:
Prevents application crashes
Improves user experience
Makes debugging easier
Protects resources
Enables monitoring
Best Practices:
Be specific about what can fail
Use custom error classes
Always cleanup with finally
Log errors for debugging
Provide meaningful error messages
Remember:
Strings don't throw errors (wrap in Error object)
Finally always executes
Errors can be caught and handled gracefully
Good error handling is a sign of professional code
Master error handling, and your applications will be more robust, reliable, and user-friendly. Every error is an opportunity to provide better feedback and create better experiences.




