Error Handling in JavaScript with try, catch, and finally

Writing a try/catch block is straightforward. The harder part is deciding which layer can actually recover, what information the error must retain, and which failures should still be allowed to surface. A catch block that merely hides the exception usually leaves the application in a more confusing state than the original error did.
How try, catch, and finally Work
At their simplest, the try, catch, and finally statements give us control over errors that occur within a block of code:
try: contains code that might throw an error.catch: catches and handles errors thrown from thetryblock.finally: runs code regardless of whether an error occurred.
Example of Basic Error Handling
Here's a fairly straightforward example:
try {
const data = JSON.parse('{invalidJson}');
console.log('Parsed data:', data);
} catch (error) {
console.error('Error parsing JSON:', error.message);
} finally {
console.log('Cleaning up...');
}If an error occurs in the try block, JavaScript immediately moves to the catch block. Regardless of an error, the finally block runs afterwards, which is useful for tasks like closing resources or resetting states.
Advanced Usage: Custom Error Types
For complex applications, custom error types can help handle errors more effectively.
Creating Custom Errors
By extending JavaScript's built‑in Error class, we can create custom error types, like this:
class ValidationError extends Error {
constructor(message: string) {
super(message);
this.name = 'ValidationError';
}
}
const validateUser = (user: { name?: string }) => {
if (!user.name) {
throw new ValidationError('Missing user name');
}
};Using a custom error:
try {
validateUser({});
} catch (error) {
if (error instanceof ValidationError) {
console.log('Validation failed:', error.message);
} else {
throw error; // rethrow unknown errors
}
}In this way, we can use custom errors to clearly communicate what went wrong, which will help us and our colleagues with code readability and maintainability.
Handling Errors Asynchronously
When working with asynchronous code, handling errors properly becomes even more crucial. JavaScript's async/await syntax integrates seamlessly with try, catch, and finally:
Example of async/await Error Handling
const fetchUserData = async (userId: number) => {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`Request failed with status ${response.status}`);
}
const user = await response.json();
return user;
} catch (error) {
throw new Error(`Could not fetch user ${userId}`, { cause: error });
} finally {
console.log('Finished user request');
}
};The catch block adds the user ID as context and rethrows with the original error as its cause. That leaves the promise rejected, so the caller can show a failure state or decide whether to retry. Logging alone would not make the request successful. The finally block runs after either outcome.
Common Pitfalls in Error Handling
Swallowing Errors
It's really important that, as developers, we avoid ignoring or "swallowing" errors, otherwise, when something does go wrong, we may have no way of working out the root cause. Always handle or explicitly log them, as unhandled errors become difficult to debug:
try {
performRiskyAction();
} catch (error) {
// Don't leave this empty!
console.error('An error occurred:', error);
}Overusing try/catch
The flip side of this is, we should use error handling thoughtfully. If we wrap every function call in try and catch blocks, we quickly end up obscuring the real source of errors. Place error‑handling code strategically, especially near code likely to fail or at application boundaries (e.g., network requests).
Testing Error Handling
Reliable applications test error handling thoroughly. Here's how you might test that a function throws an error when expected, using Jest:
test('throws ValidationError if user name is missing', () => {
expect(() => validateUser({})).toThrow(ValidationError);
});Including this type of test in your codebase ensures that errors behave predictably and help catch bugs before they reach your users.
Wrapping Up
Key Takeaways
try,catch, andfinallyprovide structured control over errors in JavaScript.- Custom errors improve clarity and control for more complex error handling.
- Always explicitly handle errors to maintain code readability and debug effectively.
async/awaitintegrates naturally with structured error handling.- Avoid ignoring errors silently; always log or handle them appropriately.
Catch an error where the code can add context, recover, or present a useful failure state. Preserve the original cause, use custom error types only when callers can act on the distinction, and test the failure path as deliberately as the successful one. If a catch block can do none of those things, it may be the wrong place to catch the error.