Difficulty: Beginner
How does error handling work in JavaScript? Explain try/catch/finally and custom error classes.
JavaScript uses try/catch/finally blocks for synchronous error handling. The try block contains code that might throw. The catch block handles the error. The finally block runs regardless of whether an error occurred - it is guaranteed to execute even if catch throws or try returns.
JavaScript has several built-in error types: Error, TypeError, ReferenceError, SyntaxError, RangeError, and URIError. Each has a name, message, and stack trace.
Custom error classes extend the built-in Error class to create domain-specific errors. This is essential in production code for distinguishing between different error types (validation errors, authentication errors, not-found errors) and handling each appropriately.
For asynchronous code, try/catch works with async/await. For promises without await, use .catch(). Unhandled rejections should be caught with a global handler.
// Basic try/catch/finally
function divide(a, b) {
try {
if (b === 0) throw new Error('Division by zero');
const result = a / b;
console.log(`Result: ${result}`);
return result;
} catch (err) {
console.log(`Error: ${err.message}`);
return null;
} finally {
console.log('Cleanup: always runs');
}
}
divide(10, 2);
console.log('---');
divide(10, 0);
// finally runs even after return
function test() {
try {
return 'from try';
} finally {
console.log('finally still runs!');
}
}
console.log(test());
finally ALWAYS executes - after return, after throw, after catch. It is perfect for cleanup like closing connections or releasing resources.
// TypeError - wrong type operation
try {
null.toString();
} catch (e) {
console.log(`${e.constructor.name}: ${e.message}`);
}
// ReferenceError - accessing undeclared variable
try {
console.log(undeclaredVar);
} catch (e) {
console.log(`${e.constructor.name}: ${e.message}`);
}
// RangeError - value out of range
try {
new Array(-1);
} catch (e) {
console.log(`${e.constructor.name}: ${e.message}`);
}
// Checking error type
try {
JSON.parse('{invalid}');
} catch (e) {
if (e instanceof SyntaxError) {
console.log('Invalid JSON:', e.message);
} else {
throw e; // re-throw unexpected errors
}
}
Use instanceof to handle specific error types differently. Re-throw errors you do not know how to handle.
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.name = 'AppError';
this.statusCode = statusCode;
}
}
class ValidationError extends AppError {
constructor(field, message) {
super(message, 400);
this.name = 'ValidationError';
this.field = field;
}
}
class NotFoundError extends AppError {
constructor(resource) {
super(`${resource} not found`, 404);
this.name = 'NotFoundError';
}
}
// Usage in an API handler
function getUser(id) {
if (!id) throw new ValidationError('id', 'User ID is required');
if (id !== 1) throw new NotFoundError('User');
return { id: 1, name: 'Alice' };
}
try {
getUser(null);
} catch (err) {
if (err instanceof ValidationError) {
console.log(`Validation: ${err.field} - ${err.message} (${err.statusCode})`);
} else if (err instanceof NotFoundError) {
console.log(`Not found: ${err.message} (${err.statusCode})`);
}
}
try {
getUser(99);
} catch (err) {
console.log(`${err.name}: ${err.message} (${err.statusCode})`);
}
Custom errors extend Error to add context like status codes and field names. instanceof checks let you handle each type differently in catch blocks.
try/catch, finally, Error Types, Custom Errors, Error Propagation