Обработка ошибок и исключений в JavaScript (Node.js): подробное руководство
Что это такое и зачем? (аналогия из реальной жизни)
Представь, что ты — капитан космического корабля. Твой корабль летит к далёкой звезде. Вдруг на приборной панели загорается красная лампочка: «Ошибка! Перегрев двигателя!» Что ты сделаешь?
- Вариант 1 (плохой): Проигнорируешь лампочку. Корабль взорвётся. Программа «упадёт».
- Вариант 2 (хороший): Ты нажимаешь кнопку «Аварийное охлаждение». Двигатель остывает, полёт продолжается. Программа «обработала ошибку».
- Вариант 3 (отличный): Ты не только включаешь охлаждение, но и записываешь в бортовой журнал: «Внимание! Двигатель №3 перегрелся. Причина: метеоритный дождь. Приняты меры». Программа «залогировала ошибку».
В программировании ошибка (exception) — это та самая красная лампочка. Она говорит: «Что-то пошло не по плану! Дальше работать нельзя, нужны срочные меры!»
Обработка ошибок (error handling) — это набор инструментов, которые позволяют программе не «взорваться» (не упасть с крашем), а красиво решить проблему: сообщить пользователю, что случилось, попробовать исправить ситуацию или хотя бы сохранить данные и завершиться корректно.
Важно: Ошибки — это не всегда плохо. Это сигналы. Хороший программист не тот, у кого нет ошибок, а тот, кто умеет их ловить и правильно на них реагировать.
В реальной жизни мы постоянно обрабатываем ошибки:
- Набрал номер — «Абонент недоступен». Ты не выбрасываешь телефон в окно, а ждёшь гудка или пишешь SMS.
- Ввёл неправильный пароль — сайт не падает, а пишет: «Неверный пароль, попробуйте снова».
- Открыл дверь — она заперта. Ты достаёшь ключ, а не ломаешь дверь с ноги.
В JavaScript (особенно в Node.js, где код работает на сервере) обработка ошибок — это вопрос выживания. Если сервер упадёт из-за одной ошибки, все пользователи останутся без сайта. Поэтому мы учимся ловить ошибки.
Теория: как это работает изнутри
Давай заглянем «под капот» JavaScript и посмотрим, как устроен механизм ошибок.
Стек вызовов (Call Stack)
Представь стопку тарелок. Когда программа вызывает функцию, она кладёт новую тарелку на стопку. Когда функция завершается — тарелку убирают. Если в функции происходит ошибка, JavaScript смотрит на верхнюю тарелку. Если там нет «ловушки» для ошибки, он снимает эту тарелку и смотрит следующую. И так до самого дна. Если на дне тоже нет ловушки — программа падает.
Типы ошибок в JavaScript
Все ошибки — это объекты. У них есть имя (name) и сообщение (message). Самые популярные:
| Тип ошибки | Когда возникает | Пример |
|---|---|---|
ReferenceError |
Обратились к переменной, которой нет | console.log(a) (a не объявлена) |
TypeError |
Пытаемся сделать что-то с данными неправильного типа | null.name (у null нет свойств) |
SyntaxError |
Нарушен синтаксис языка (опечатка в коде) | if (true {} (пропущена скобка) |
RangeError |
Выход за допустимые пределы | new Array(-1) (отрицательная длина массива) |
URIError |
Неправильное использование функций работы с URI | decodeURI('%') |
Как JavaScript «бросает» ошибку
Когда происходит что-то плохое, JavaScript создаёт объект ошибки и бросает (throw) его. Это как если бы ты подбросил в воздух горячую картошку — кто-то должен её поймать, иначе она упадёт и разобьётся.
Мы тоже можем бросать ошибки вручную с помощью команды throw. Это полезно, когда мы сами хотим сказать: «Стоп! Дальше работать нельзя!»
Синтаксис в JavaScript (Node.js): все формы и варианты
Главный инструмент для ловли ошибок — конструкция try...catch...finally. Давай разберём её по частям.
Базовая конструкция: try...catch
try {
// Здесь пишем код, который может сломаться
// Если ошибка произойдёт — выполнение try прерывается
// и управление переходит в catch
} catch (error) {
// Сюда попадает объект ошибки
// Здесь мы решаем, что делать: сообщить пользователю,
// записать в лог, попробовать другой способ
}
Добавляем finally
Блок finally выполняется ВСЕГДА — и если ошибка была, и если её не было. Это как уборка после вечеринки: даже если всё прошло отлично, ты всё равно выносишь мусор.
try {
// Пытаемся что-то сделать
} catch (error) {
// Ловим ошибку
} finally {
// Этот код выполнится в любом случае
// Например: закрыть файл, отключиться от базы данных
}
Бросаем свои ошибки: throw
Мы можем создать свою ошибку и «бросить» её:
throw new Error('Моё сообщение об ошибке');
// Можно использовать разные типы:
throw new TypeError('Должно быть число!');
throw new RangeError('Слишком большое значение!');
Типы ошибок в Node.js (специфичные)
В Node.js есть свои системные ошибки, которые возникают при работе с файлами, сетью и т.д.:
| Код ошибки | Описание |
|---|---|
ENOENT |
Файл или папка не найдены |
EACCES |
Нет прав доступа |
EADDRINUSE |
Порт уже занят |
ECONNREFUSED |
Соединение отклонено |
Примеры кода — от простого к сложному
Пример 1: Ловим ReferenceError
// Самый простой пример: пытаемся использовать несуществующую переменную
try {
// Пытаемся вывести то, чего нет
console.log(несуществующаяПеременная);
// Эта строка НЕ выполнится, потому что выше ошибка
console.log('Этот текст не увидим');
} catch (ошибка) {
// Ловим ошибку и выводим информацию о ней
console.log('Поймали ошибку!');
console.log('Имя ошибки:', ошибка.name); // ReferenceError
console.log('Сообщение:', ошибка.message); // несуществующаяПеременная is not defined
console.log('Стек вызовов:', ошибка.stack); // Где произошла ошибка
}
// Программа продолжает работать!
console.log('Программа жива!');
Пример 2: Ловим TypeError при работе с объектами
// Частая ошибка: пытаемся получить свойство у null или undefined
let пользователь = null; // Представим, что пользователь не найден в базе
try {
// Пытаемся получить имя пользователя
let имя = пользователь.name;
console.log('Имя пользователя:', имя);
} catch (ошибка) {
if (ошибка instanceof TypeError) {
console.log('Ошибка типа! Скорее всего, объект равен null или undefined');
console.log('Сообщение:', ошибка.message);
} else {
// Если ошибка другого типа — пробрасываем дальше
throw ошибка;
}
}
// Даём пользователю дружелюбное сообщение
console.log('Пользователь не найден. Пожалуйста, войдите в систему.');
Пример 3: Бросаем свою ошибку с throw
// Функция, которая проверяет возраст пользователя
function проверитьВозраст(возраст) {
// Если возраст не число — бросаем ошибку
if (typeof возраст !== 'number') {
throw new TypeError('Возраст должен быть числом!');
}
// Если возраст меньше 0 или больше 150 — бросаем ошибку
if (возраст < 0 || возраст > 150) {
throw new RangeError('Возраст должен быть от 0 до 150 лет!');
}
// Если всё хорошо — возвращаем true
return true;
}
// Используем нашу функцию с обработкой ошибок
try {
let результат = проверитьВозраст(-5);
console.log('Возраст подходит:', результат);
} catch (ошибка) {
console.log('Ошибка проверки возраста:');
console.log('Тип:', ошибка.name);
console.log('Сообщение:', ошибка.message);
}
// Попробуем с корректным значением
try {
let результат = проверитьВозраст(25);
console.log('Возраст подходит:', результат);
} catch (ошибка) {
console.log('Ошибка:', ошибка.message);
}
Пример 4: finally — уборка после себя
// Представим, что мы работаем с файлом (в реальности используют fs)
let файлОткрыт = false;
function открытьФайл() {
console.log('Файл открыт');
файлОткрыт = true;
}
function закрытьФайл() {
console.log('Файл закрыт');
файлОткрыт = false;
}
function прочитатьФайл() {
// Имитируем ошибку чтения
throw new Error('Ошибка чтения файла! Диск повреждён.');
}
// Основная работа
открытьФайл();
try {
console.log('Пытаемся прочитать файл...');
прочитатьФайл();
console.log('Файл прочитан успешно'); // Эта строка не выполнится
} catch (ошибка) {
console.log('Поймали ошибку:', ошибка.message);
} finally {
// Этот блок выполнится ВСЕГДА!
// Даже если ошибка была, мы должны закрыть файл
console.log('finally: обязательно закрываем файл');
закрытьФайл();
}
console.log('Файл открыт?', файлОткрыт); // false — файл точно закрыт
Пример 5: Вложенные try...catch
// Иногда нужно ловить ошибки на разных уровнях
function уровень1() {
try {
console.log('Уровень 1: начинаем работу');
уровень2();
console.log('Уровень 1: завершаем работу'); // Не выполнится
} catch (ошибка) {
console.log('Уровень 1: поймал ошибку из уровня 2');
console.log('Сообщение:', ошибка.message);
// Можем добавить свою информацию
throw new Error('Уровень 1 не смог обработать: ' + ошибка.message);
}
}
function уровень2() {
console.log('Уровень 2: начинаем работу');
// Здесь нет try...catch, поэтому ошибка уйдёт на уровень 1
throw new Error('Ошибка на уровне 2!');
}
// Вызываем верхний уровень
try {
уровень1();
} catch (ошибка) {
console.log('Главный обработчик: поймал окончательную ошибку');
console.log('Финальное сообщение:', ошибка.message);
}
Пример 6: Работа с асинхронными ошибками (callback)
// В Node.js много асинхронного кода
// try...catch НЕ ловит ошибки в асинхронных callback'ах!
const fs = require('fs');
// НЕПРАВИЛЬНО: try не сработает
try {
fs.readFile('/несуществующий/файл.txt', 'utf8', (err, data) => {
// Эта ошибка произойдёт ПОСЛЕ того, как try уже завершился
if (err) {
throw err; // Этот throw никто не поймает!
}
console.log(data);
});
} catch (ошибка) {
// Сюда мы НИКОГДА не попадём!
console.log('Поймали?', ошибка.message);
}
// ПРАВИЛЬНО: обрабатываем ошибку внутри callback
fs.readFile('/несуществующий/файл.txt', 'utf8', (err, data) => {
if (err) {
console.log('Ошибка чтения файла:');
console.log('Код ошибки:', err.code); // ENOENT
console.log('Сообщение:', err.message);
return; // Выходим из callback
}
console.log('Содержимое файла:', data);
});
console.log('Программа продолжает работу...');
Пример 7: Обработка ошибок в промисах (Promise)
// Современный способ работы с асинхронностью — промисы
function получитьДанные(id) {
return new Promise((resolve, reject) => {
// Имитируем запрос к серверу
setTimeout(() => {
if (id < 0) {
// Если id отрицательный — отклоняем промис
reject(new Error('ID не может быть отрицательным!'));
} else {
// Всё хорошо — возвращаем данные
resolve({ id: id, name: 'Пользователь ' + id });
}
}, 1000);
});
}
// Способ 1: .catch()
получитьДанные(-1)
.then(данные => {
console.log('Получены данные:', данные);
})
.catch(ошибка => {
console.log('Ошибка получения данных:');
console.log('Сообщение:', ошибка.message);
});
// Способ 2: async/await с try...catch (самый современный)
async function показатьПользователя(id) {
try {
let данные = await получитьДанные(id);
console.log('Пользователь найден:', данные.name);
} catch (ошибка) {
console.log('Не удалось получить пользователя:');
console.log('Причина:', ошибка.message);
} finally {
console.log('Запрос завершён (успешно или с ошибкой)');
}
}
// Вызываем асинхронную функцию
показатьПользователя(42);
показатьПользователя(-5);
Сравнение с Python: чем похоже и чем отличается
Если ты уже знаешь Python или планируешь его учить, вот сравнение:
| Характеристика | JavaScript (Node.js) | Python |
|---|---|---|
| Базовая конструкция | try { ... } catch (err) { ... } |
try: ... except Exception as e: ... |
| Блок "всегда" | finally { ... } |
finally: ... |
| Бросок ошибки | throw new Error('msg') |
raise Exception('msg') |
| Типы ошибок | Error, TypeError, ReferenceError и др. |
Exception, TypeError, ValueError, KeyError и др. |
| Ловля конкретной ошибки | catch (err) { if (err instanceof TypeError) ... } |
except TypeError: ... |
| Асинхронные ошибки | Отдельные механизмы: callback, Promise, async/await | Асинхронность через asyncio, похожие принципы |
| Стек вызовов | err.stack |
traceback.format_exc() |
Главные отличия:
- В Python можно ловить несколько типов ошибок в одном блоке:
except (TypeError, ValueError):. В JS нужно проверять черезinstanceof. - В Python есть блок
else, который выполняется, если ошибки не было. В JS такого нет. - В JS асинхронность встроена в язык, и обработка ошибок в асинхронном коде — отдельная большая тема.
Типичные ошибки и подводные камни
1. try...catch не ловит асинхронные ошибки
Это самая частая ошибка новичков. Если внутри try есть setTimeout, fs.readFile или другой асинхронный код — ошибка произойдёт после того, как try уже завершился.
// НЕПРАВИЛЬНО
try {
setTimeout(() => {
throw new Error('Асинхронная ошибка');
}, 1000);
} catch (e) {
console.log('Не поймает!');
}
// ПРАВИЛЬНО
setTimeout(() => {
try {
throw new Error('Асинхронная ошибка');
} catch (e) {
console.log('Поймали внутри callback');
}
}, 1000);
2. Слишком общий catch
Ловить все ошибки подряд — плохая практика. Ты можешь случайно «проглотить» ошибку, которую нужно было обработать по-другому.
// ПЛОХО: ловим всё подряд
try {
// какой-то код
} catch (e) {
console.log('Ошибка:', e.message); // Слишком общее сообщение
}
// ХОРОШО: обрабатываем разные типы по-разному
try {
// какой-то код
} catch (e) {
if (e instanceof TypeError) {
console.log('Проблема с типом данных');
} else if (e instanceof ReferenceError) {
console.log('Обращение к несуществующей переменной');
} else {
// Неизвестная ошибка — пробрасываем дальше
throw e;
}
}
3. Забыли про finally
Если ты открыл файл, подключился к базе данных или занял какой-то ресурс — обязательно освободи его в finally. Иначе ресурс «утечёт».
4. Бросаем не объект Error
Можно бросить что угодно: строку, число, объект. Но лучше всегда использовать new Error(), чтобы сохранить стек вызовов.
// ПЛОХО: теряем информацию
throw 'Ошибка!';
// ХОРОШО: сохраняем стек
throw new Error('Ошибка!');
5. Пустой catch
Никогда не оставляй catch пустым. Даже если ты просто логируешь ошибку — это лучше, чем молча её проглотить.
// ОЧЕНЬ ПЛОХО
try {
// код
} catch (e) {} // Ошибка исчезла бесследно
// ЛУЧШЕ
try {
// код
} catch (e) {
console.error('Произошла ошибка:', e);
}
Лучшие практики и советы профессионалов
1. Используй конкретные типы ошибок
Создавай свои классы ошибок для разных ситуаций:
// Создаём свой тип ошибки
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = 'ValidationError';
}
}
class DatabaseError extends Error {
constructor(message, query) {
super(message);
this.name = 'DatabaseError';
this.query = query; // Дополнительная информация
}
}
// Используем
try {
throw new ValidationError('Email не может быть пустым');
} catch (e) {
if (e instanceof ValidationError) {
console.log('Ошибка валидации:', e.message);
}
}
2. Всегда логируй ошибки
В Node.js используй console.error() для ошибок. В серьёзных проектах — специальные библиотеки для логирования (winston, pino).
try {
// опасный код
} catch (e) {
// Логируем с полной информацией
console.error('Ошибка в модуле загрузки:', {
message: e.message,
stack: e.stack,
time: new Date().toISOString()
});
// Пользователю показываем только безопасную информацию
console.log('Что-то пошло не так. Пожалуйста, попробуйте позже.');
}
3. Не используй try...catch для управления потоком
Ошибки — это исключительные ситуации, а не обычный способ проверки условий.
// ПЛОХО: используем ошибку для проверки
try {
let result = JSON.parse(userInput);
} catch (e) {
console.log('Некорректный JSON');
}
// ХОРОШО: проверяем перед использованием
try {
let result = JSON.parse(userInput);
// работаем с result
} catch (e) {
console.log('Некорректный JSON');
}
// ЕЩЁ ЛУЧШЕ: валидируем заранее
function isValidJSON(str) {
try {
JSON.parse(str);
return true;
} catch {
return false;
}
}
if (isValidJSON(userInput)) {
let result = JSON.parse(userInput);
} else {
console.log('Пожалуйста, введите корректный JSON');
}
4. Пробрасывай ошибки, которые не можешь обработать
Если ты не знаешь, что делать с ошибкой — не прячь её. Пусть она уйдёт выше, где её смогут обработать.
function загрузитьДанные() {
try {
let data = fetchDataFromAPI();
return data;
} catch (e) {
// Мы не знаем, как обработать эту ошибку
// Но можем добавить контекст
throw new Error('Не удалось загрузить данные: ' + e.message);
}
}
// Вызывающий код решит, что делать
try {
let data = загрузитьДанные();
} catch (e) {
console.log('Критическая ошибка:', e.message);
process.exit(1); // Завершаем программу
}
5. Используй process.on('uncaughtException') с осторожностью
В Node.js есть глобальный обработчик необработанных ошибок. Но использовать его для «спасения» программы — плохая практика.
// Только для логирования и корректного завершения!
process.on('uncaughtException', (error) => {
console.error('Необработанная ошибка!', error);
// Делаем последние действия: сохраняем данные, закрываем соединения
// И завершаем процесс
process.exit(1);
});
Вопросы для самопроверки
-
Что произойдёт, если в блоке try возникнет ошибка, а catch отсутствует?
Ответ: Ошибка «всплывёт» по стеку вызовов. Если нигде нет catch, программа завершится аварийно (uncaught exception).
-
В каком порядке выполняются блоки try, catch, finally?
Ответ: Сначала выполняется try. Если ошибки нет — finally. Если ошибка есть — catch, потом finally. finally выполняется всегда.
-
Можно ли использовать throw без try...catch?
Ответ: Да, можно. Но тогда ошибку должен поймать кто-то выше по стеку. Если никто не поймает — программа упадёт.
-
Чем отличается ReferenceError от TypeError?
Ответ: ReferenceError — когда переменная не существует. TypeError — когда операция недопустима для данного типа (например, чтение свойства у null).
-
Почему try...catch не ловит ошибки в setTimeout?
Ответ: Потому что setTimeout — асинхронная операция. Код внутри setTimeout выполняется после того, как try уже завершился. Ошибку нужно ловить внутри самого setTimeout.
-
Что такое «проброс ошибки» (rethrowing)?
Ответ: Это когда в catch мы не обрабатываем ошибку, а бросаем её дальше с помощью throw. Так мы передаём ответственность выше по стеку.
-
Как создать свою ошибку с дополнительными полями?
Ответ: Создать класс, наследующий от Error, и добавить нужные поля. Например:
class MyError extends Error { constructor(msg, code) { super(msg); this.code = code; } } -
Что выведет этот код?
try { throw new Error('Ой!'); } catch (e) { console.log('Поймал'); } finally { console.log('Финал'); } console.log('После');Ответ: Сначала «Поймал», потом «Финал», потом «После». Программа не падает.
-
В чём разница между
console.logиconsole.error?Ответ:
console.logвыводит в стандартный поток вывода (stdout).console.error— в поток ошибок (stderr). В консоли это выглядит одинаково, но в системах логирования их можно разделять. -
Нужно ли обрабатывать каждую ошибку в программе?
Ответ: Нет, не каждую. Некоторые ошибки должны «упасть» и показать разработчику, что код сломан. Но все ошибки, которые могут возникнуть в production, должны быть обработаны.
Итог и что изучить дальше
Мы прошли большой путь! Давай подведём итоги:
- Ошибки — это объекты с именем, сообщением и стеком вызовов.
- try...catch...finally — главный инструмент для обработки синхронных ошибок.
- throw позволяет создавать и бросать свои ошибки.
- Асинхронные ошибки требуют особого подхода: callback, Promise.catch(), async/await.
- Лучшие практики: используй конкретные типы ошибок, логируй, не глотай ошибки молча, всегда освобождай ресурсы в finally.
Что изучить дальше?
- Промисы (Promise) — основа современной асинхронности в JS. Узнай, как работают цепочки .then().catch().
- async/await — синтаксический сахар над промисами. Позволяет писать асинхронный код как синхронный.
- Событие 'error' в Node.js — многие объекты (потоки, серверы) генерируют события ошибок.
- Библиотеки для логирования — winston, pino, bunyan. В серьёзных проектах простого console.error недостаточно.
- Тестирование ошибок — как писать тесты, которые проверяют, что ошибки выбрасываются и обрабатываются правильно.
- Паттерн «Результат» (Result type) — аль