Введение: Почему обработка ошибок — основа надёжного кода
Любое приложение, работающее на Node.js, рано или поздно сталкивается с исключительными ситуациями: неверный ввод пользователя, сбой сети, отсутствие файла на диске или некорректные данные из базы. Без грамотной обработки ошибок эти ситуации приводят к падению сервера, утечке памяти или неопределённому поведению. В JavaScript, особенно в среде Node.js, механизм try/catch является фундаментальным инструментом для построения отказоустойчивых систем. В этой статье мы разберём не только классический try/catch, но и продвинутые техники: кастомные классы ошибок, работу с промисами и асинхронными функциями, а также лучшие практики логирования.
Основы: блоки try, catch и finally
Базовый синтаксис обработки ошибок в JavaScript выглядит так:
try {
// Код, который может выбросить исключение
const data = JSON.parse('{"name": "Node.js"}');
console.log(data.name);
} catch (error) {
// Обработка ошибки
console.error('Произошла ошибка:', error.message);
} finally {
// Код, который выполнится в любом случае
console.log('Блок finally выполнен');
}
Как это работает:
try— содержит потенциально опасный код.catch— перехватывает исключение, если оно возникло. В параметрerrorпопадает объект ошибки.finally— выполняется всегда, независимо от того, была ошибка или нет. Идеально для закрытия файлов, освобождения ресурсов или завершения транзакций.
Важно помнить: finally сработает даже после return в блоке try или catch.
Типы ошибок в JavaScript
Встроенные конструкторы ошибок помогают точнее классифицировать исключения:
Error— базовый класс для всех ошибок.SyntaxError— синтаксическая ошибка (например, некорректный JSON).TypeError— неверный тип данных (например, вызов не функции).ReferenceError— обращение к несуществующей переменной.RangeError— выход за допустимые пределы (например, рекурсия).
Пример обработки разных типов:
try {
const obj = null;
console.log(obj.property); // TypeError
} catch (err) {
if (err instanceof TypeError) {
console.log('Ошибка типа:', err.message);
} else if (err instanceof ReferenceError) {
console.log('Переменная не найдена');
} else {
console.log('Неизвестная ошибка:', err);
}
}
Генерация собственных ошибок: оператор throw
Иногда необходимо явно указать, что в программе возникла исключительная ситуация. Для этого используется оператор throw. Он может выбросить что угодно: строку, число, объект, но лучше всего — экземпляр класса Error или его наследника.
function divide(a, b) {
if (b === 0) {
throw new Error('Деление на ноль запрещено');
}
if (typeof a !== 'number' || typeof b !== 'number') {
throw new TypeError('Аргументы должны быть числами');
}
return a / b;
}
try {
const result = divide(10, 0);
console.log(result);
} catch (err) {
console.error(err.name + ': ' + err.message);
}
Почему лучше выбрасывать Error, а не строку? Объект Error содержит стек вызовов (stack), что критически важно для отладки в Node.js. Строка такой информации не даст.
Создание кастомных классов ошибок
В реальных проектах (например, веб-сервер на Express) удобно иметь собственные типы ошибок для разных ситуаций: ValidationError, NotFoundError, DatabaseError. Это упрощает централизованную обработку.
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = 'ValidationError';
this.statusCode = 400;
}
}
class NotFoundError extends Error {
constructor(resource) {
super(`Ресурс "${resource}" не найден`);
this.name = 'NotFoundError';
this.statusCode = 404;
}
}
// Использование
function getUser(id) {
if (!id) throw new ValidationError('ID пользователя обязателен');
// ... логика поиска
throw new NotFoundError('user');
}
try {
getUser(null);
} catch (err) {
if (err instanceof ValidationError) {
console.log('Ошибка валидации:', err.message);
} else if (err instanceof NotFoundError) {
console.log('Не найдено:', err.message);
}
console.log('HTTP статус:', err.statusCode);
}
Асинхронная обработка ошибок: промисы и async/await
Node.js построен на асинхронности. Для промисов существует два основных способа обработки ошибок:
1. Метод .catch() для промисов
const fs = require('fs').promises;
fs.readFile('/path/to/file.txt', 'utf8')
.then(data => {
console.log('Файл прочитан:', data);
})
.catch(err => {
console.error('Ошибка чтения файла:', err.message);
});
2. try/catch с async/await
Современный и самый читаемый подход:
const fs = require('fs').promises;
async function readConfig() {
try {
const data = await fs.readFile('config.json', 'utf8');
const config = JSON.parse(data);
return config;
} catch (err) {
if (err.code === 'ENOENT') {
console.error('Файл конфигурации не найден');
} else if (err instanceof SyntaxError) {
console.error('Некорректный JSON в конфиге');
} else {
console.error('Неизвестная ошибка:', err);
}
throw err; // Пробрасываем дальше, если не можем обработать
}
}
readConfig().catch(err => {
console.log('Фатальная ошибка при запуске:', err.message);
process.exit(1);
});
Обратите внимание: в catch можно пробросить ошибку дальше (throw err), если текущий уровень не может её корректно обработать. Это формирует цепочку ответственности.
Лучшие практики и частые ошибки
Вот несколько