⚡ JavaScript

Обработка ошибок и исключений в 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);
});

Вопросы для самопроверки

  1. Что произойдёт, если в блоке try возникнет ошибка, а catch отсутствует?

    Ответ: Ошибка «всплывёт» по стеку вызовов. Если нигде нет catch, программа завершится аварийно (uncaught exception).

  2. В каком порядке выполняются блоки try, catch, finally?

    Ответ: Сначала выполняется try. Если ошибки нет — finally. Если ошибка есть — catch, потом finally. finally выполняется всегда.

  3. Можно ли использовать throw без try...catch?

    Ответ: Да, можно. Но тогда ошибку должен поймать кто-то выше по стеку. Если никто не поймает — программа упадёт.

  4. Чем отличается ReferenceError от TypeError?

    Ответ: ReferenceError — когда переменная не существует. TypeError — когда операция недопустима для данного типа (например, чтение свойства у null).

  5. Почему try...catch не ловит ошибки в setTimeout?

    Ответ: Потому что setTimeout — асинхронная операция. Код внутри setTimeout выполняется после того, как try уже завершился. Ошибку нужно ловить внутри самого setTimeout.

  6. Что такое «проброс ошибки» (rethrowing)?

    Ответ: Это когда в catch мы не обрабатываем ошибку, а бросаем её дальше с помощью throw. Так мы передаём ответственность выше по стеку.

  7. Как создать свою ошибку с дополнительными полями?

    Ответ: Создать класс, наследующий от Error, и добавить нужные поля. Например: class MyError extends Error { constructor(msg, code) { super(msg); this.code = code; } }

  8. Что выведет этот код?

    try {
        throw new Error('Ой!');
    } catch (e) {
        console.log('Поймал');
    } finally {
        console.log('Финал');
    }
    console.log('После');

    Ответ: Сначала «Поймал», потом «Финал», потом «После». Программа не падает.

  9. В чём разница между console.log и console.error?

    Ответ: console.log выводит в стандартный поток вывода (stdout). console.error — в поток ошибок (stderr). В консоли это выглядит одинаково, но в системах логирования их можно разделять.

  10. Нужно ли обрабатывать каждую ошибку в программе?

    Ответ: Нет, не каждую. Некоторые ошибки должны «упасть» и показать разработчику, что код сломан. Но все ошибки, которые могут возникнуть в production, должны быть обработаны.

Итог и что изучить дальше

Мы прошли большой путь! Давай подведём итоги:

  • Ошибки — это объекты с именем, сообщением и стеком вызовов.
  • try...catch...finally — главный инструмент для обработки синхронных ошибок.
  • throw позволяет создавать и бросать свои ошибки.
  • Асинхронные ошибки требуют особого подхода: callback, Promise.catch(), async/await.
  • Лучшие практики: используй конкретные типы ошибок, логируй, не глотай ошибки молча, всегда освобождай ресурсы в finally.

Что изучить дальше?

  1. Промисы (Promise) — основа современной асинхронности в JS. Узнай, как работают цепочки .then().catch().
  2. async/await — синтаксический сахар над промисами. Позволяет писать асинхронный код как синхронный.
  3. Событие 'error' в Node.js — многие объекты (потоки, серверы) генерируют события ошибок.
  4. Библиотеки для логирования — winston, pino, bunyan. В серьёзных проектах простого console.error недостаточно.
  5. Тестирование ошибок — как писать тесты, которые проверяют, что ошибки выбрасываются и обрабатываются правильно.
  6. Паттерн «Результат» (Result type) — аль