Learn to handle runtime errors gracefully with try/catch/finally, throw custom errors, and understand error types.
Runtime errors are a fact of life in programming. Without proper handling, a single error can crash your entire program and leave users staring at a broken page.
Not all errors are bugs in your code. Many come from external sources you cannot control:
null or undefinedError handling allows your program to detect these problems and respond gracefully instead of crashing:
// Without error handling — crashes!
const data = JSON.parse('invalid json'); // SyntaxError!
console.log('This never runs');
// With error handling — recovers gracefully
try {
const data = JSON.parse('invalid json');
} catch (error) {
console.log('Invalid data, using defaults');
const data = {};
}
console.log('Program continues running');Good error handling improves user experience (showing helpful messages instead of blank screens), debugging (logging meaningful error info), and reliability (keeping the app running when things go wrong).
The try...catch statement lets you attempt risky code and handle any errors that occur:
try {
// Code that might throw an error
const result = riskyOperation();
console.log(result);
} catch (error) {
// Code that runs if an error is thrown
console.log('Something went wrong:', error.message);
}The error object in the catch block has useful properties:
error.message — a human-readable description of the errorerror.name — the error type (e.g., 'TypeError', 'SyntaxError')error.stack — a stack trace showing where the error occurred (useful for debugging)The finally block runs no matter what — whether the code succeeded or failed. It is perfect for cleanup operations:
let connection;
try {
connection = openDatabase();
const data = connection.query('SELECT * FROM users');
processData(data);
} catch (error) {
console.log('Database error:', error.message);
} finally {
// Always close the connection, even if there was an error
if (connection) {
connection.close();
}
}You can also nest try...catch blocks for more granular error handling:
try {
try {
JSON.parse(input);
} catch (parseError) {
console.log('Parse failed, trying alternative...');
JSON.parse(alternativeInput);
}
} catch (error) {
console.log('All parsing failed:', error.message);
}You can throw your own errors using the throw statement. This is useful for enforcing validation rules and function contracts:
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'
}JavaScript has several built-in error types, each signaling a different kind of problem:
TypeError — wrong type: null.property, calling a non-functionReferenceError — using an undefined variableRangeError — value out of range: new Array(-1)SyntaxError — invalid code: JSON.parse('{')URIError — malformed URI: decodeURI('%')You can throw specific error types for clarity:
function setAge(age) {
if (typeof age !== 'number') {
throw new TypeError('Age must be a number');
}
if (age < 0 || age > 150) {
throw new RangeError('Age must be between 0 and 150');
}
return age;
}You can also create custom error classes for your application:
class ValidationError extends Error {
constructor(field, message) {
super(message);
this.name = 'ValidationError';
this.field = field;
}
}
throw new ValidationError('email', 'Invalid email format');Here are practical patterns for handling errors effectively:
1. Validate input early (guard clauses):
function processUser(user) {
if (!user) throw new Error('User is required');
if (!user.email) throw new Error('Email is required');
// ... proceed with valid data
}2. Wrap risky operations in try/catch:
function loadConfig(jsonString) {
try {
return JSON.parse(jsonString);
} catch (error) {
console.warn('Invalid config, using defaults');
return { theme: 'dark', language: 'en' };
}
}3. Do not catch errors you cannot handle — let them bubble up to a higher-level handler:
function readFile(path) {
// If this fails, let the caller handle it
// Don't catch and silently swallow the error
return fs.readFileSync(path, 'utf8');
}4. Re-throw errors when you need to log but not fully handle them:
try {
processPayment(order);
} catch (error) {
console.error('Payment failed:', error);
throw error; // re-throw for the caller to handle
}5. Error boundaries — in frameworks like React, you can catch rendering errors at a component level so one broken component does not crash the entire app. The concept applies broadly: catch errors at appropriate boundaries in your application.
<div id="output"></div>
<script>
function safeParseJSON(str) {
try {
const data = JSON.parse(str);
return { success: true, data };
} catch (error) {
return { success: false, error: error.message };
}
}
// Test with valid JSON
const valid = safeParseJSON('{"name": "Alice", "age": 30}');
// Test with invalid JSON
const invalid = safeParseJSON('not valid json');
const output = document.getElementById('output');
output.innerHTML = `
<p><strong>Valid JSON:</strong> ${valid.success ? valid.data.name : 'Failed'}</p>
<p><strong>Invalid JSON:</strong> ${invalid.success ? 'Parsed' : invalid.error}</p>
`;
</script>When does the finally block execute?