Обработка ошибок в JavaScript: try/catch и не только
Обработка ошибок — одна из ключевых задач при разработке на JavaScript, особенно в среде Node.js, где стабильность серверного приложения напрямую влияет на пользовательский опыт. Необработанные исключения могут привести к падению всего процесса и потере данных. В этой статье мы разберём не только классический блок try/catch, но и продвинутые техники: работу с finally, создание кастомных типов ошибок, обработку асинхронных ошибок и лучшие практики для Node.js.
1. Основы: try, catch и finally
Блок try/catch — фундаментальный механизм перехвата синхронных исключений. Если в блоке try возникает ошибка, выполнение немедленно передаётся в catch. Блок finally выполняется всегда — независимо от того, была ошибка или нет. Это удобно для освобождения ресурсов (закрытие файлов, соединений).
const fs = require('fs');
function readConfig(path) {
try {
const data = fs.readFileSync(path, 'utf8');
console.log('Конфигурация загружена');
return JSON.parse(data);
} catch (err) {
console.error('Ошибка при чтении конфига:', err.message);
return null;
} finally {
console.log('Попытка чтения завершена');
}
}
const config = readConfig('./config.json');
console.log(config); // null при ошибке
Важно: catch перехватывает только ошибки, возникшие в try. Ошибки в catch или finally остаются необработанными.
2. Типы ошибок в JavaScript и Node.js
В JavaScript существует несколько встроенных типов ошибок, каждый из которых наследуется от Error:
- SyntaxError — синтаксическая ошибка парсинга кода.
- ReferenceError — обращение к несуществующей переменной.
- TypeError — недопустимый тип данных (например, вызов не функции).
- RangeError — значение вне допустимого диапазона.
- URIError — ошибка при работе с URI (encodeURI/decodeURI).
В Node.js добавлены AssertionError (из модуля assert) и системные ошибки (EACCES, ENOENT и др.) из модуля fs, которые содержат код ошибки в свойстве code.
try {
require('fs').readFileSync('/nonexistent');
} catch (err) {
if (err.code === 'ENOENT') {
console.log('Файл не найден');
} else {
console.log('Другая ошибка:', err.message);
}
}
3. Создание кастомных ошибок
Для более точного контроля над логикой обработки рекомендуется создавать собственные классы ошибок, расширяющие Error. Это позволяет различать ошибки бизнес-логики от системных сбоев.
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = 'ValidationError';
this.field = field;
}
}
class DatabaseError extends Error {
constructor(message, query) {
super(message);
this.name = 'DatabaseError';
this.query = query;
}
}
function saveUser(user) {
if (!user.email) {
throw new ValidationError('Email обязателен', 'email');
}
// имитация ошибки БД
throw new DatabaseError('Таймаут соединения', 'INSERT INTO users');
}
try {
saveUser({ name: 'Иван' });
} catch (err) {
if (err instanceof ValidationError) {
console.log(`Поле ${err.field}: ${err.message}`);
} else if (err instanceof DatabaseError) {
console.error(`Ошибка БД (${err.query}): ${err.message}`);
// повторная попытка или логирование
} else {
console.error('Неизвестная ошибка:', err);
}
}
Использование instanceof позволяет строить цепочки обработчиков, не прибегая к проверке строковых кодов.
4. Асинхронные ошибки: Promises и async/await
В Node.js большинство операций асинхронны. Ошибки в промисах обрабатываются через .catch() или блок try/catch с async/await. Важно помнить: try/catch не ловит ошибки в обычных колбэках — только в асинхронных функциях.
// Пример с async/await
const fetch = require('node-fetch');
async function fetchUser(id) {
try {
const response = await fetch(`https://api.example.com/users/${id}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (err) {
console.error('Ошибка запроса:', err.message);
throw err; // пробрасываем дальше
}
}
// Глобальный обработчик необработанных промисов
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
// process.exit(1); // опционально
});
Необработанные отклонённые промисы (unhandledRejection) — одна из частых причин утечек памяти и нестабильности. В Node.js 15+ они приводят к завершению процесса по умолчанию.
5. Лучшие практики для Node.js
- Не подавляйте ошибки — пустой
catch(без логирования) скрывает проблемы. - Используйте централизованный обработчик — middleware для Express или глобальный
process.on('uncaughtException'). - Всегда обрабатывайте reject'ы — добавляйте
.catch()в конец цепочек промисов. - Структурируйте ошибки — используйте кастомные классы и свойство
code. - Логируйте стек —
console.error(err.stack)даёт полный контекст. - Не выбрасывайте не-Error объекты —
throw 'string'ломает стеки и инструменты.
// Пример централизованного обработчика в Express
function errorHandler(err, req, res, next) {
console.error(err.stack);
if (err instanceof ValidationError) {
return res.status(400).json({ error: err.message, field: err