javascript-today

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);
});