Introduction: Why Error Handling is the Foundation of Reliable Code
Errors in code are inevitable. They arise from invalid user data, network failures, missing files on the server, or simple developer typos. In the Node.js environment, where code executes asynchronously and often interacts with external resources (databases, file system, APIs), an unhandled error can crash the entire process. Proper error handling in JavaScript turns a fragile application into a resilient service. In this article, we will cover all key mechanisms: from classic try/catch to creating custom error classes and working with asynchronous code.
1. Basics: try, catch, finally and throw
The basic block for exception handling in JavaScript is the try...catch...finally construct. It allows you to "catch" an error that occurred in a "dangerous" section of code and respond to it correctly without interrupting the execution of the entire program.
Syntax and operation of try/catch
Code that might throw an exception is placed inside the try block. If an error occurs, control is immediately transferred to the catch block, where the error object is available. If there is no error, the catch block is ignored.
try { const data = JSON.parse('{"name": "Anna"}'); // Valid JSON console.log(data.name); // Outputs: Anna} catch (error) { console.error('Parsing error:', error.message);}// Execution continuesNow let's simulate an error:
try { const data = JSON.parse('invalid json'); // Error! console.log(data.name);} catch (error) { console.error('Parsing error:', error.message); // Output: Parsing error: Unexpected token 'i', ... is not valid JSON}console.log('Program is alive!'); // This code will executeThe finally block: code that always runs
The finally block executes regardless of whether an error occurred or not. It is ideal for releasing resources (closing files, database connections) or cleaning up timers.
let resource = null;try { resource = acquireResource(); // Acquire resource resource.doSomething(); // Dangerous operation} catch (error) { console.error('Error:', error);} finally { if (resource) { resource.close(); // Always close the resource console.log('Resource released.'); }}The throw operator: creating your own errors
You can "throw" an error yourself using throw. This is useful for validating input data or interrupting execution when business logic is violated. You can pass anything as an argument, but the best practice is to use the Error object or its descendants.
function divide(a, b) { if (typeof a !== 'number' || typeof b !== 'number') { throw new Error('Arguments must be numbers'); } if (b === 0) { throw new Error('Division by zero is not allowed'); } return a / b;}
try { console.log(divide(10, 0));} catch (error) { console.error('Error:', error.message); // Error: Division by zero is not allowed}2. Advanced Techniques: Custom Error Classes and Typing
In large projects, simply using the standard Error is not enough. You need to distinguish between network errors, validation errors, authorization errors, and others. For this, custom classes extending the built-in Error are created.
Creating custom errors
class ValidationError extends Error { constructor(message) { super(message); this.name = 'ValidationError'; // Unique name for checking }}
class NetworkError extends Error { constructor(message, statusCode) { super(message); this.name = 'NetworkError'; this.statusCode = statusCode; }}
function fetchUser(id) { if (!id) { throw new Valida