Error Handling - Exercise 2
You can create a custom error class by extending the built in Error class. This allows you to define your own error types with custom names, messages, and additional properties for better error handling.
// Create a custom error class
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = 'ValidationError';
}
}
// Create another custom error with extra properties
class HttpError extends Error {
constructor(message, statusCode) {
super(message);
this.name = 'HttpError';
this.statusCode = statusCode;
}
}
// Using custom errors
throw new ValidationError('Email is required');
throw new HttpError('Not Found', 404);
You can rethrow an error by using the throw statement inside a catch block. This is useful when you want to log the error or perform some action, but still let the error propagate to a higher level handler.
function processData(data) {
try {
// Some operation that might fail
if (!data) {
throw new Error('No data provided');
}
return JSON.parse(data);
} catch (error) {
// Log the error first
console.log('Error occurred:', error.message);
// Rethrow the same error
throw error;
}
}
try {
processData(null);
} catch (error) {
console.log('Caught rethrown error:', error.message);
}
// Output: Error occurred: No data provided
// Output: Caught rethrown error: No data provided
Nested try...catch blocks allow you to handle errors at different levels. The inner catch handles errors from the inner try block, and if it rethrows, the outer catch can handle it. Each level can handle errors differently.
try {
console.log('Outer try block');
try {
console.log('Inner try block');
throw new Error('Inner error');
} catch (innerError) {
console.log('Inner catch:', innerError.message);
// Rethrow to outer catch
throw new Error('Wrapped: ' + innerError.message);
}
} catch (outerError) {
console.log('Outer catch:', outerError.message);
}
// Output:
// Outer try block
// Inner try block
// Inner catch: Inner error
// Outer catch: Wrapped: Inner error
JSON.parse throws a SyntaxError when the input is not valid JSON. Wrapping it in a try...catch block allows you to handle invalid JSON gracefully and provide a fallback value or error message.
function parseJSON(jsonString) {
try {
const data = JSON.parse(jsonString);
console.log('Parsed successfully:', data);
return data;
} catch (error) {
console.log('Invalid JSON:', error.message);
return null;
}
}
// Valid JSON
parseJSON('{"name": "John", "age": 30}');
// Output: Parsed successfully: { name: 'John', age: 30 }
// Invalid JSON
parseJSON('{name: John}');
// Output: Invalid JSON: Unexpected token n in JSON
// Empty string
parseJSON('');
// Output: Invalid JSON: Unexpected end of JSON input
Errors in callbacks cannot be caught by a try...catch around the function that schedules the callback. The common pattern is to pass the error as the first argument to the callback function, known as error first callbacks.
// Error first callback pattern
function fetchData(id, callback) {
if (!id) {
callback(new Error('ID is required'), null);
return;
}
// Simulate successful fetch
callback(null, { id: id, name: 'Item ' + id });
}
// Using the callback with error handling
fetchData(null, function(error, data) {
if (error) {
console.log('Error:', error.message);
return;
}
console.log('Data:', data);
});
// Output: Error: ID is required
fetchData(5, function(error, data) {
if (error) {
console.log('Error:', error.message);
return;
}
console.log('Data:', data);
});
// Output: Data: { id: 5, name: 'Item 5' }
A try...catch block around setTimeout will not catch errors thrown inside the callback because the callback runs asynchronously. You must place the try...catch inside the setTimeout callback itself.
// This will NOT catch the error
try {
setTimeout(function() {
throw new Error('Async error');
}, 100);
} catch (error) {
console.log('Caught:', error.message); // Never runs
}
// Correct way: try...catch inside setTimeout
setTimeout(function() {
try {
throw new Error('Async error');
} catch (error) {
console.log('Caught inside:', error.message);
}
}, 100);
// Output: Caught inside: Async error
// Using a wrapper function
function safeTimeout(callback, delay) {
setTimeout(function() {
try {
callback();
} catch (error) {
console.log('Error in timeout:', error.message);
}
}, delay);
}
The stack trace is a list of function calls that shows where an error occurred and the sequence of function calls that led to it. Each line shows the function name, file name, and line number, read from top to bottom.
function first() {
second();
}
function second() {
third();
}
function third() {
throw new Error('Something went wrong');
}
try {
first();
} catch (error) {
console.log(error.stack);
}
// Stack trace output:
// Error: Something went wrong
// at third (script.js:10:9) <-- Error thrown here
// at second (script.js:6:3) <-- Called by second
// at first (script.js:2:3) <-- Called by first
// at script.js:14:3 <-- Original call
// Access stack as a string
console.log(error.stack);
You can validate function arguments at the start of a function and throw descriptive errors when validation fails. This helps catch bugs early and provides clear error messages about what went wrong.
function divide(a, b) {
// Validate arguments
if (typeof a !== 'number' || typeof b !== 'number') {
throw new TypeError('Both arguments must be numbers');
}
if (b === 0) {
throw new RangeError('Cannot divide by zero');
}
return a / b;
}
try {
console.log(divide(10, 2)); // 5
console.log(divide(10, 0)); // Throws RangeError
} catch (error) {
console.log(error.name + ': ' + error.message);
}
// Output: RangeError: Cannot divide by zero
try {
console.log(divide('10', 2)); // Throws TypeError
} catch (error) {
console.log(error.name + ': ' + error.message);
}
// Output: TypeError: Both arguments must be numbers
You can check the error type using instanceof inside a catch block. This lets you handle different error types differently, providing specific responses for each type of error.
function process(value) {
if (typeof value !== 'number') {
throw new TypeError('Expected a number');
}
if (value < 0) {
throw new RangeError('Value must be positive');
}
return value * 2;
}
try {
process(-5);
} catch (error) {
if (error instanceof TypeError) {
console.log('Type error:', error.message);
} else if (error instanceof RangeError) {
console.log('Range error:', error.message);
} else if (error instanceof SyntaxError) {
console.log('Syntax error:', error.message);
} else {
console.log('Unknown error:', error.message);
}
}
// Output: Range error: Value must be positive
Both throw Error() and throw new Error() create an Error object and work the same way. The Error constructor can be called with or without new. However, using new is the recommended convention for clarity and consistency.
// Both create an Error object
const error1 = Error('Without new');
const error2 = new Error('With new');
console.log(error1 instanceof Error); // true
console.log(error2 instanceof Error); // true
// Both can be thrown
try {
throw Error('Without new keyword');
} catch (e) {
console.log(e.message); // "Without new keyword"
}
try {
throw new Error('With new keyword');
} catch (e) {
console.log(e.message); // "With new keyword"
}
// Recommended: Always use new for clarity
throw new Error('This is the preferred way');
The console.error method outputs error messages to the console with special formatting. It displays messages in red with an error icon, making them easy to spot. It also shows a stack trace in most browsers.
// Basic error logging
console.error('Something went wrong!');
// Log error objects
try {
throw new Error('Test error');
} catch (error) {
console.error('Caught error:', error);
console.error('Error message:', error.message);
console.error('Stack trace:', error.stack);
}
// Log multiple values
const userId = 123;
const action = 'delete';
console.error('Failed action:', action, 'for user:', userId);
// Difference from console.log
console.log('This is a normal log'); // Normal text
console.error('This is an error log'); // Red text with icon
// Use with conditional checks
function validate(data) {
if (!data) {
console.error('Validation failed: data is missing');
return false;
}
return true;
}
When working with arrays, you should check if the value is actually an array and if indexes are valid. JavaScript does not throw errors for out of bounds access, so you need to validate manually.
function getItem(arr, index) {
// Check if it is an array
if (!Array.isArray(arr)) {
throw new TypeError('First argument must be an array');
}
// Check if array is empty
if (arr.length === 0) {
throw new Error('Array is empty');
}
// Check if index is valid
if (index < 0 || index >= arr.length) {
throw new RangeError('Index out of bounds: ' + index);
}
return arr[index];
}
const fruits = ['apple', 'banana', 'cherry'];
try {
console.log(getItem(fruits, 1)); // "banana"
console.log(getItem(fruits, 10)); // Throws RangeError
} catch (error) {
console.log(error.name + ': ' + error.message);
}
// Output: RangeError: Index out of bounds: 10
try {
console.log(getItem('not an array', 0)); // Throws TypeError
} catch (error) {
console.log(error.name + ': ' + error.message);
}
// Output: TypeError: First argument must be an array