Error Handling & Debugging
Why error handling matters
Every program encounters errors. Users type unexpected input, APIs go down, network connections drop. The difference between a good app and a bad one is how it handles these situations.
Types of errors
Syntax errors
The code is written incorrectly — JavaScript can't even parse it:
JavaScript
// Missing closing parenthesis
console.log('hello'
// SyntaxError: Unexpected end of inputYour editor catches most of these before you run the code.
Runtime errors
The code is valid but something goes wrong during execution:
JavaScript
const user = null;
console.log(user.name);
// TypeError: Cannot read properties of null (reading 'name')Logical errors
The code runs without crashing but produces wrong results:
JavaScript
function calculateDiscount(price, percent) {
return price * percent; // Bug: should be price * (percent / 100)
}
calculateDiscount(100, 20); // 2000, not 20These are the hardest to find because JavaScript doesn't tell you anything is wrong.
try/catch/finally
JavaScript
try {
const data = JSON.parse(userInput);
processData(data);
} catch (error) {
console.error('Invalid JSON:', error.message);
showUserError('Please enter valid data.');
} finally {
hideLoadingSpinner();
}try— wraps code that might fail.catch— runs if an error occurs. Theerrorobject hasmessageandstackproperties.finally— always runs, whether there was an error or not. Perfect for cleanup.
When to use try/catch
Use it around operations that can fail for reasons outside your control:
- Parsing JSON (
JSON.parse) - Network requests (
fetch) - Accessing storage (
localStorage) - Third-party library calls
Throwing custom errors
JavaScript
function divide(a, b) {
if (b === 0) {
throw new Error('Cannot divide by zero');
}
return a / b;
}
try {
divide(10, 0);
} catch (error) {
console.error(error.message); // "Cannot divide by zero"
}Custom error classes
For larger apps, create specific error types:
JavaScript
class ValidationError extends Error {
constructor(field, message) {
super(message);
this.name = 'ValidationError';
this.field = field;
}
}
class NotFoundError extends Error {
constructor(resource) {
super(`${resource} not found`);
this.name = 'NotFoundError';
this.status = 404;
}
}
function getUser(id) {
const user = database.find(u => u.id === id);
if (!user) throw new NotFoundError('User');
return user;
}Now you can catch specific error types:
JavaScript
try {
const user = getUser(999);
} catch (error) {
if (error instanceof NotFoundError) {
showNotFound();
} else if (error instanceof ValidationError) {
showValidationError(error.field, error.message);
} else {
throw error; // re-throw unexpected errors
}
}Async error handling
With
async/await, use try/catch:JavaScript
async function loadData() {
try {
const response = await fetch('/api/data');
if (!response.ok) {
throw new Error(`Server returned ${response.status}`);
}
return await response.json();
} catch (error) {
if (error.name === 'AbortError') {
console.log('Request was cancelled');
} else {
console.error('Failed to load:', error.message);
throw error;
}
}
}Debugging with console
Beyond
console.log, there are better tools:JavaScript
console.error('Something broke'); // red output
console.warn('Careful here'); // yellow output
console.table([{ a: 1 }, { a: 2 }]); // formatted table
console.group('User data'); // collapsible group
console.log('Name:', name);
console.log('Age:', age);
console.groupEnd();
console.time('fetch'); // start timer
await fetch('/api/data');
console.timeEnd('fetch'); // "fetch: 142ms"Debugging with browser DevTools
Breakpoints
Instead of adding
console.log everywhere, set breakpoints:- Open DevTools (F12).
- Go to the Sources tab.
- Click a line number to set a breakpoint.
- When execution hits that line, it pauses.
- Inspect variables in the Scope panel.
- Step through code line-by-line.
debugger; in your code:JavaScript
function processOrder(order) {
debugger; // execution pauses here when DevTools is open
const total = calculateTotal(order);
return total;
}Network tab
The Network tab shows every HTTP request your app makes. You can see:
- Request/response headers
- Request body
- Response data
- Timing (how long each request took)
- Status codes
Defensive coding patterns
Guard clauses
JavaScript
function processUser(user) {
if (!user) return;
if (!user.email) return;
sendEmail(user.email);
}Return early for invalid states instead of nesting everything in if/else.
Default values
JavaScript
function greet(name = 'stranger') {
return `Hello, ${name}!`;
}Input validation
JavaScript
function createAccount(email, password) {
if (typeof email !== 'string' || !email.includes('@')) {
throw new ValidationError('email', 'Invalid email address');
}
if (!password || password.length < 8) {
throw new ValidationError('password', 'Password must be at least 8 characters');
}
}Key takeaway
Good error handling means anticipating what can go wrong and providing useful feedback. Use
try/catch for operations that can fail externally, throw custom errors for domain logic, use guard clauses for defensive coding, and learn your browser DevTools — they're more powerful than console.log.