Введение: Почему обработка ошибок — это фундамент надежного кода
Ошибки в коде неизбежны. Они возникают из-за некорректных данных пользователя, сбоев сети, отсутствия файлов на сервере или банальных опечаток разработчика. В среде Node.js, где код выполняется асинхронно и часто взаимодействует с внешними ресурсами (базы данных, файловая система, API), необработанная ошибка может привести к падению всего процесса. Грамотная обработка ошибок в JavaScript превращает хрупкое приложение в устойчивый сервис. В этой статье мы разберем все ключевые механизмы: от классического try/catch до создания собственных классов ошибок и работы с асинхронным кодом.
1. Основы: try, catch, finally и throw
Базовый блок для обработки исключений в JavaScript — это конструкция try...catch...finally. Она позволяет "поймать" ошибку, которая возникла в "опасном" участке кода, и корректно на нее отреагировать, не прерывая выполнение всей программы.
Синтаксис и работа try/catch
Код, который может выбросить исключение, помещается в блок try. Если ошибка возникает, управление мгновенно передается в блок catch, где доступен объект ошибки. Если ошибки нет, блок catch игнорируется.
try {
const data = JSON.parse('{"name": "Анна"}'); // Корректный JSON
console.log(data.name); // Выведет: Анна
} catch (error) {
console.error('Ошибка парсинга:', error.message);
}
// Выполнение продолжается
Теперь смоделируем ошибку:
try {
const data = JSON.parse('невалидный json'); // Ошибка!
console.log(data.name);
} catch (error) {
console.error('Ошибка парсинга:', error.message);
// Вывод: Ошибка парсинга: Unexpected token 'н', ... is not valid JSON
}
console.log('Программа жива!'); // Этот код выполнится
Блок finally: код, который выполнится всегда
Блок finally выполняется независимо от того, была ли ошибка или нет. Он идеально подходит для освобождения ресурсов (закрытие файлов, соединений с БД) или очистки таймеров.
let resource = null;
try {
resource = acquireResource(); // Получаем ресурс
resource.doSomething(); // Опасная операция
} catch (error) {
console.error('Ошибка:', error);
} finally {
if (resource) {
resource.close(); // Всегда закрываем ресурс
console.log('Ресурс освобожден.');
}
}
Оператор throw: создаем свои ошибки
Вы можете сами "бросить" ошибку с помощью throw. Это полезно для валидации входных данных или прерывания выполнения при нарушении бизнес-логики. В качестве аргумента можно передать что угодно, но лучшая практика — использовать объект Error или его наследников.
function divide(a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new Error('Аргументы должны быть числами');
}
if (b === 0) {
throw new Error('Деление на ноль запрещено');
}
return a / b;
}
try {
console.log(divide(10, 0));
} catch (error) {
console.error('Ошибка:', error.message); // Ошибка: Деление на ноль запрещено
}
2. Продвинутые техники: Собственные классы ошибок и типизация
В больших проектах просто использовать стандартный Error недостаточно. Нужно различать ошибки сети, валидации, авторизации и другие. Для этого создают пользовательские классы, расширяющие встроенный Error.
Создание кастомных ошибок
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = 'ValidationError'; // Уникальное имя для проверки
}
}
class NetworkError extends Error {
constructor(message, statusCode) {
super(message);
this.name = 'NetworkError';
this.statusCode = statusCode;
}
}
function fetchUser(id) {
if (!id) {
throw new ValidationError('ID пользователя обязателен');
}
// Имитация сетевого запроса
if (id < 0) {
throw new NetworkError('Сервер недоступен', 500);
}
return { id, name: 'Иван' };
}
try {
fetchUser(-1);
} catch (error) {
if (error instanceof ValidationError) {
console.log('Ошибка валидации:', error.message);
} else if (error instanceof NetworkError) {
console.log(`Сетевая ошибка (${error.statusCode}): ${error.message}`);
} else {
console.log('Неизвестная ошибка:', error);
}
}
Проверка типа ошибки
Вместо instanceof иногда используют свойство name или код ошибки. instanceof предпочтительнее, так как он работает с иерархией классов.
3. Асинхронные ошибки: Callbacks, Promises и Async/Await
Node.js построен на асинхронности. Ошибки в асинхронном коде не могут быть пойманы try/catch на том же уровне, что и синхронный код. Рассмотрим эволюцию подходов.
Эпоха Callbacks (Node.js-style)
В старом стиле первым аргументом коллбэка всегда передавалась ошибка (error-first callback).
const fs = require('fs');
fs.readFile('/path/to/file.txt', 'utf8', (err, data) => {
if (err) {
console.error('Не удалось прочитать файл:', err.message);
return;
}
console.log(data);
});
Promises: .catch()
Промисы позволяют обрабатывать ошибки цепочкой .catch().
const fs = require('fs').promises;
fs.readFile('/path/to/file.txt', 'utf8')
.then(data => {
console.log(data);
})
.catch(error => {
console.error('Ошибка при чтении файла:', error.message);
});
Async/Await: try/catch для асинхронного кода
Синтаксис async/await вернул возможность использовать привычный try/catch для асинхронных операций. Это самый читаемый и современный способ.
const fs = require('fs').promises;
async function readConfig() {
try {
const data = await fs.readFile('config.json', 'utf8');
const config = JSON.parse(data);
console.log('