Чек-лист по Node.js для новичков: обработка ошибок

Ошибки — неизбежная часть разработки, но в Node.js они могут не просто сломать приложение, а привести к утечкам данных, бесконечным зависаниям или даже сбоям всего сервера. Поэтому обработка потенциальных ошибок — не опциональный шаг, а ключевой элемент надежного кода. Сегодня разберем, какие бывают ошибки в Node, как правильно с ними работать и что иногда упускают новички.

272 открытий2К показов
Чек-лист по Node.js для новичков: обработка ошибок

Ошибки — неизбежная часть разработки, но в Node.js они могут не просто сломать приложение, а привести к утечкам данных, бесконечным зависаниям или даже сбоям всего сервера. Поэтому обработка потенциальных ошибок — не опциональный шаг, а ключевой элемент надежного кода. Сегодня разберем, какие бывают ошибки в Node, как правильно с ними работать и что иногда упускают новички.

Типы ошибок в Node.js и основные подходы к их обработке

Синхронные ошибки

В экосистеме Node синхронные ошибки представляют собой исключения, которые возникают в процессе выполнения кода и приводят к немедленному завершению программы, если их не обработать. Встречаются в тех случаях, когда код выполняется последовательно, без использования асинхронных операций. Разработчик, взаимодействующий с Node с нуля, должен понимать природу таких ошибок и уметь эффективно организовывать их обработку.

Если такая ошибка не перехвачена, она приведёт к аварийному завершению процесса. Какие есть примеры?

Ошибки типа (TypeError, ReferenceError)

			function sayHello(name) {
    console.log("Hello, " + name.toUpperCase());
}

sayHello(null); // TypeError: Cannot read properties of null (reading 'toUpperCase')

		

Они возникают, когда код обращается к переменной, которая не определена, или передаёт в функцию аргумент неподходящего типа.

Ошибки парсинга JSON

			const data = "{ invalid json }";
try {
    JSON.parse(data);
} catch (error) {
    console.error("Ошибка парсинга JSON:", error.message);
}

		

Если переданный JSON-строковый объект невалиден, JSON.parse() выбросит исключение.

Деление на ноль или некорректные математические операции

			function divide(a, b) {
    if (b === 0) {
        throw new Error("Деление на ноль невозможно");
    }
    return a / b;
}

try {
    divide(10, 0);
} catch (error) {
    console.error(error.message);
}

		

JavaScript допускает деление на ноль, но оно может приводить к логическим ошибкам.

Но как правильно обрабатывать синхронные ошибки?

Используем try...catch

Классический способ перехвата ошибок в синхронном коде — использование конструкции try...catch.

			try {
    let result = someUndefinedFunction();
} catch (error) {
    console.error("Ошибка выполнения кода:", error.message);
}

		

Генерируем пользовательские ошибки

Можно явно выбрасывать ошибки с пояснением:

			function validateInput(value) {
    if (typeof value !== "string") {
        throw new TypeError("Ожидается строка в качестве входных данных");
    }
}

try {
    validateInput(42);
} catch (error) {
    console.error("Ошибка валидации:", error.message);
}

		

Используем process.on('uncaughtException', callback)

Node позволяет перехватывать необработанные ошибки глобально:

			process.on("uncaughtException", (error) => {
    console.error("Необработанное исключение:", error.message);
});

throw new Error("Критическая ошибка");

		

Синхронные ошибки в Node могут возникать по разным причинам: неверный тип данных, ошибки в логике или работе с JSON и т.д.. При работе важно помнить, что их можно перехватывать try...catch, выбрасывать вручную с пояснениями и даже обрабатывать глобально с помощью process.on(). Это снижает вероятность неожиданного завершения работы сервера и повышает надежность приложений.

Асинхронные ошибки

В отличие от синхронных, асинхронные ошибки в Node не прерывают выполнение кода немедленно, а возникают в процессе работы асинхронных операций. Это делает их обработку более сложной, поскольку ошибки могут всплывать после завершения основной логики программы.

Асинхронные ошибки в Node могут проявляться в трёх основных сценариях:

  1. Ошибки в коллбэках;
  2. Ошибки в промисах;
  3. Ошибки в async/await.

Разработчик должен уметь обрабатывать потенциальные ошибки во всех этих случаях, чтобы избежать непредсказуемого поведения приложения.

Ошибки в коллбэках

Коллбэк-функции — один из старейших способов работы с асинхронностью в Node. Однако при их использовании легко столкнуться с ошибками, если не следить за правильной передачей аргументов и выполнением условий.

Пример: ошибка внутри коллбэка

			const fs = require("fs");

fs.readFile("nonexistent.txt", "utf8", (err, data) => {
    if (err) {
        console.error("Ошибка чтения файла:", err.message);
        return;
    }
    console.log("Содержимое файла:", data);
});

		

Здесь, если файл nonexistent.txt отсутствует, в err передается ошибка, и программа не падает, а просто выводит сообщение. Однако, если бы в коде не было проверки if (err), приложение завершилось бы при попытке обработать data.

Как правильно обрабатывать ошибки в коллбэках?

  1. Всегда проверять аргумент err;
  2. Передавать ошибки дальше, если не знаем, как с ними работать.

Пример обработки ошибки и её проброса:

			function readFileCallback(err, data) {
    if (err) {
        return console.error("Ошибка обработки файла:", err.message);
    }
    console.log("Файл успешно прочитан:", data);
}

fs.readFile("data.txt", "utf8", readFileCallback);

		

Ошибки в промисах

Промисы — более современный способ работы с асинхронностью в Node. Они позволяют избежать глубокой вложенности (callback hell) и обеспечивают удобный механизм обработки ошибок.

Пример: промис с ошибкой

			const fetchData = () => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            reject(new Error("Ошибка загрузки данных"));
        }, 1000);
    });
};

fetchData()
    .then((data) => console.log("Данные получены:", data))
    .catch((error) => console.error("Ошибка при загрузке:", error.message));

		

Здесь, если в fetchData произойдёт ошибка (reject вызывается с new Error()), она будет перехвачена в .catch().

Как правильно обрабатывать ошибки в промисах?

  1. Использовать .catch() для перехвата ошибок;
  2. Использовать .finally(), если нужно выполнить код в любом случае;
  3. Не забывать возвращать промисы из функций.

Пример корректной обработки:

			const fetchUserData = (userId) => {
    return new Promise((resolve, reject) => {
        if (!userId) {
            return reject(new Error("ID пользователя не указан"));
        }
        setTimeout(() => resolve({ id: userId, name: "Alice" }), 500);
    });
};

fetchUserData(null)
    .then((user) => console.log("Пользователь:", user))
    .catch((err) => console.error("Ошибка:", err.message))
    .finally(() => console.log("Завершение операции"));

		

Ошибки в async/await

async/await — современный способ работы с асинхронным кодом, который позволяет писать его в стиле синхронного. Однако ошибки в таком коде не всегда очевидны, особенно если промисы не обернуты в try...catch.

Пример ошибки в async/await:

			const getUser = async () => {
    throw new Error("Ошибка при получении пользователя");
};

const main = async () => {
    const user = await getUser(); // Ошибка не обработана → программа завершится
    console.log(user);
};

main();

		

Если getUser() выбросит ошибку, программа аварийно завершится. Чтобы избежать этого, нужно использовать try...catch.

Как правильно обрабатывать ошибки в async/await?

  1. Оборачивать вызовы await в try...catch;
  2. Использовать catch() для перехвата на уровне вызова.

Пример правильной обработки:

			const getUserSafe = async () => {
    try {
        const user = await getUser();
        console.log("Пользователь:", user);
    } catch (error) {
        console.error("Ошибка в getUserSafe:", error.message);
    }
};

getUserSafe();

		

Или можно обработать ошибку прямо в .catch():

			getUser().then(console.log).catch((err) => console.error("Ошибка:", err.message));

		

Асинхронные ошибки в Node могут проявляться в коллбэках, промисах и async/await. Каждый из этих подходов требует своей стратегии обработки ошибок:

  • В коллбэках всегда проверяйте err;
  • В промисах используйте .catch() и .finally();
  • В async/await оборачивайте код в try...catch.

Грамотная обработка ошибок в асинхронном коде — залог стабильности и предсказуемости работы Node-приложения.

Чек-лист для новичков: как правильно обрабатывать ошибки в Node.js

Ошибки в коде неизбежны, но грамотная обработка позволяет минимизировать их влияние на работу приложения. Даже если вероятность проблемы кажется низкой, её игнорирование может привести к неожиданным сбоям, утечке данных и того хуже. Рассмотрим, какие шаги должен предпринять новичок, чтобы сделать своё приложение в Node.js устойчивым к ошибкам.

Всегда обрабатывайте ошибки, даже если они маловероятны

Игнорирование ошибок может привести к фатальным последствиям: например, неожиданное исключение может привести к падению всего сервера. Поэтому всегда старайтесь обрабатывать даже маловероятные ошибки.

Пример ошибки без обработки:

			const data = JSON.parse('{"name": "John"'); // Ошибка: отсутствует закрывающая кавычка
console.log(data.name);

		

Этот код вызовет исключение SyntaxError и остановит выполнение программы.

Правильный вариант с обработкой:

			try {
    const data = JSON.parse('{"name": "John"');
    console.log(data.name);
} catch (error) {
    console.error("Ошибка парсинга JSON:", error.message);
}

		

Даже если кажется, что JSON всегда будет корректным, лучше обработать его парсинг в try...catch, чтобы избежать сбоев.

Используйте централизованный обработчик ошибок (middleware в Express)

Если в вашем приложении на Express.js ошибки обрабатываются в каждом обработчике маршрута отдельно, это приведёт к дублированию кода. Вместо этого используйте централизованный middleware.

Что делать с централизованным обработчиком ошибок в Express?

  1. Определите middleware для обработки ошибок;
  2. Все ошибки передавайте через next(error);
  3. Express автоматически передаст ошибку в обработчик.

Пример:

			const express = require("express");
const app = express();

// Обычный маршрут
app.get("/", (req, res, next) => {
    try {
        throw new Error("Что-то пошло не так!");
    } catch (error) {
        next(error); // Передаём ошибку в централизованный обработчик
    }
});

// Централизованный обработчик ошибок
app.use((err, req, res, next) => {
    console.error(err.stack);
    res.status(500).json({ message: "Ошибка на сервере", error: err.message });
});

app.listen(3000, () => console.log("Сервер запущен на порту 3000"));

		

Теперь любая ошибка, возникшая в обработчиках маршрутов, будет передаваться в этот middleware, логироваться и возвращать корректный ответ клиенту.

Логируйте ошибки (например, с помощью Winston или Pino)

Логирование ошибок помогает отслеживать сбои и анализировать их причины. В продакшен-приложениях нельзя ограничиваться console.log() — лучше использовать специализированные библиотеки.

Как работает Winston?

Winston — это гибкий логгер для Node.js, поддерживающий сохранение логов в файлы, базу данных и удалённые сервисы.

Установка:

			npm install winston

		

Настройка логгера:

			const winston = require("winston");

const logger = winston.createLogger({
    level: "error",
    transports: [
        new winston.transports.File({ filename: "errors.log" }),
        new winston.transports.Console(),
    ],
});

module.exports = logger;

		

Использование в коде:

			try {
    throw new Error("Критическая ошибка!");
} catch (error) {
    logger.error(error.message);
}

		

Теперь ошибки будут логироваться как в консоли, так и в файле errors.log.

Генерируйте пользовательские ошибки через Error (или свои классы ошибок)

Вместо простых строковых сообщений об ошибках используйте объекты Error или создавайте собственные классы ошибок для лучшей структуризации.

Простой пример с Error:

			function validateUser(user) {
    if (!user.name) {
        throw new Error("Имя пользователя обязательно!");
    }
}

		

Создание собственного класса ошибок:

			class ValidationError extends Error {
    constructor(message) {
        super(message);
        this.name = "ValidationError";
    }
}

function validateUser(user) {
    if (!user.name) {
        throw new ValidationError("Имя пользователя обязательно!");
    }
}

try {
    validateUser({});
} catch (error) {
    console.error(`${error.name}: ${error.message}`);
}

		

Использование классов ошибок делает код более читаемым и удобным для отладки.

Не забывайте про process.on('unhandledRejection') и process.on('uncaughtException')

В приложениях на Node.js есть два типа необработанных ошибок, которые могут привести к завершению процесса:

  • unhandledRejection — когда промис отклонён (Promise.reject()), но у него нет обработчика .catch();
  • uncaughtException — когда код выбрасывает исключение, но оно не обрабатывается try...catch.

Чтобы защититься, добавьте обработчики для глобальных ошибок:

			process.on("unhandledRejection", (reason, promise) => {
    console.error("Необработанное отклонение промиса:", reason);
});

process.on("uncaughtException", (error) => {
    console.error("Необработанное исключение:", error);
    process.exit(1); // Завершаем процесс, чтобы избежать нестабильного состояния
});

		

Эти обработчики помогут отлавливать неожиданные ошибки и предотвращать аварийное завершение программы.

Добавьте тесты для проверки поведения вашего кода при ошибках

Тестирование обработки ошибок помогает убедиться, что приложение корректно реагирует на сбои. Поэтому используйте Jest для тестирования.

Пример теста на обработку ошибок с Jest:

			const { validateUser } = require("./userValidation");

test("должен выбрасывать ValidationError, если нет имени", () => {
    expect(() => validateUser({})).toThrow("Имя пользователя обязательно!");
});

		

Регулярные тесты помогут выявлять проблемы до развертывания в продакшен.

Резюмируем:

  •  Обрабатывайте ошибки, даже если они маловероятны — лучше перестраховаться, чем допустить неожиданный сбой.
  •  Используйте централизованный обработчик ошибок (middleware) в Express — это упростит код и улучшит поддержку.
  •  Логируйте ошибки (Winston, Pino) — так вы сможете отслеживать и анализировать сбои.
  •  Создавайте собственные классы ошибок — это сделает код чище и понятнее.
  •  Обрабатывайте глобальные ошибки (unhandledRejection, uncaughtException) — чтобы сервер не падал неожиданно.
  •  Пишите тесты на обработку ошибок — так ваш код будет надёжнее.

Практические примеры: обработка ошибок в Node.js

Ошибки — неотъемлемая часть разработки, и их грамотная обработка играет ключевую роль в создании надёжных приложений. Рассмотрим практические примеры реализации обработки ошибок в API на Express, создания пользовательских классов ошибок и настройки глобального обработчика.

Обработка ошибок в API с использованием Express

Для примера создадим маршрут, который вызовет ошибку, и настроим обработчик:

			const express = require("express");
const app = express();

app.use(express.json());

// Маршрут с искусственной ошибкой
app.get("/user", (req, res, next) => {
    try {
        throw new Error("Пользователь не найден!");
    } catch (error) {
        next(error); // Передаём ошибку в централизованный обработчик
    }
});

// Централизованный обработчик ошибок
app.use((err, req, res, next) => {
    console.error("Ошибка:", err.message);
    res.status(500).json({ error: err.message });
});

// Запуск сервера
app.listen(3000, () => {
    console.log("Сервер запущен на порту 3000");
});

		

Что здесь важно?

  • Передача ошибок через next(error) — это стандартный способ уведомить Express о том, что произошла ошибка;
  • Централизованный обработчик (middleware) позволяет управлять ошибками в одном месте.

Пример пользовательского класса ошибок

Иногда стандартного объекта Error недостаточно для описания специфики ошибки. В таких случаях можно создать собственный класс:

			class NotFoundError extends Error {
    constructor(message) {
        super(message);
        this.name = "NotFoundError";
        this.statusCode = 404;
    }
}

// Использование пользовательской ошибки
app.get("/product/:id", (req, res, next) => {
    const productId = req.params.id;

    // Допустим, продукт не найден
    if (!productId) {
        return next(new NotFoundError("Продукт не найден"));
    }

    res.json({ id: productId, name: "Продукт" });
});

// Централизованный обработчик ошибок с проверкой типа
app.use((err, req, res, next) => {
    if (err instanceof NotFoundError) {
        return res.status(err.statusCode).json({ error: err.message });
    }
    res.status(500).json({ error: "Что-то пошло не так" });
});

		

Что здесь важно?

  • Так можно чётко разделять типы ошибок;
  • Так можно упростить добавление новой логики обработки для каждого типа ошибок.

Как настроить глобальный обработчик ошибок

Некоторые ошибки не обрабатываются в отдельных блоках try...catch или маршрутах. Для таких ситуаций нужны глобальные обработчики ошибок.

Глобальная обработка unhandledRejection и uncaughtException

Глобальные обработчики защищают приложение от аварийных завершений:

			process.on("unhandledRejection", (reason, promise) => {
    console.error("Необработанное отклонение промиса:", reason);
});

process.on("uncaughtException", (error) => {
    console.error("Необработанное исключение:", error);
    process.exit(1); // Завершаем процесс, чтобы избежать нестабильного состояния
});

		

Что здесь важно?

  • unhandledRejection — позволяет обрабатывать промисы без catch;
  • uncaughtException — защищает приложение от ошибок, не пойманных в try...catch.

Пример с промисом:

			new Promise((resolve, reject) => {
    reject("Ошибка в промисе");
});
// Без глобального обработчика программа вылетит с ошибкой.

		

Обработка ошибок — обязательная часть разработки в Node.js, особенно для приложений в продакшене. Рассмотренные примеры помогут программисту:

  • Правильно организовать обработку ошибок в API;
  • Создавать пользовательские классы ошибок для улучшения читаемости и поддержки кода;
  • Настраивать глобальные обработчики, чтобы защитить приложение от необработанных ошибок.

Частые ошибки при обработке ошибок в Node.js

Даже опытные разработчики иногда совершают ошибки при обработке. Недочёты могут привести к неожиданным сбоям, утечкам данных или усложнению отладки. Рассмотрим наиболее распространённые ошибки, которые допускают программисты, и разберём, как их избежать.

Пропуск ошибок в асинхронных функциях

Асинхронный код в Node.js требует особого внимания, поскольку ошибки в промисах и async/await могут не перехватываться автоматически.

Ошибка: нет обработки ошибок в промисах

			const fs = require("fs").promises;

// Функция без обработки ошибок
async function readFileContent() {
    const data = await fs.readFile("nonexistent.txt", "utf-8");
    console.log(data);
}

readFileContent(); // Ошибка просто "упадёт" в консоль

		

Если файл не существует, программа завершится с ошибкой UnhandledPromiseRejectionWarning.

Правильный вариант: обработка через try...catch

			async function readFileContent() {
    try {
        const data = await fs.readFile("nonexistent.txt", "utf-8");
        console.log(data);
    } catch (error) {
        console.error("Ошибка чтения файла:", error.message);
    }
}
readFileContent();

		

Теперь даже если файл отсутствует, приложение не вылетит, а отобразит сообщение об ошибке.

Запоминаем:

  • Всегда используйте try...catch внутри async-функций;
  • При использовании промисов добавляйте .catch(), чтобы предотвратить unhandledRejection;
  • Используйте глобальный обработчик process.on("unhandledRejection"), если работаете с большим количеством промисов.

Логирование конфиденциальных данных

Логирование — полезная практика, но если неосторожно записывать ошибки в логи, можно случайно сохранить конфиденциальные данные — пароли, токены или платежные данные.

Ошибка: логирование чувствительных данных

			app.post("/login", async (req, res) => {
    try {
        const { username, password } = req.body;
        const user = await findUser(username, password);
        res.json({ success: true, user });
    } catch (error) {
        console.error("Ошибка при входе:", error, "Пароль:", req.body.password);
        res.status(500).json({ error: "Ошибка сервера" });
    }
});

		

Ошибка здесь в том, что в логи попадает пароль пользователя.

Правильный вариант: фильтрация логов

			app.post("/login", async (req, res) => {
    try {
        const { username, password } = req.body;
        const user = await findUser(username, password);
        res.json({ success: true, user });
    } catch (error) {
        console.error("Ошибка при входе:", error.message);
        res.status(500).json({ error: "Ошибка сервера" });
    }
});

		

Запоминаем:

  • Не логируйте req.body, req.headers, req.query без фильтрации;
  • Используйте безопасные библиотеки для логирования, например, winston или pino, с возможностью маскировки данных;
  • Избегайте передачи ошибок клиенту без обработки — error.message может содержать внутренние данные системы.

Попытка поглощать ошибки вместо их корректной обработки

Некоторые разработчики, чтобы избежать сбоев, просто подавляют ошибки, не анализируя их причины. Это приводит к тому, что ошибки замалчиваются, а баги остаются незамеченными.

Ошибка: пустой catch-блок

			async function fetchData() {
    try {
        const response = await fetch("https://api.example.com/data");
        return await response.json();
    } catch (error) {
        // Просто игнорируем ошибку
    }
}

		

Здесь ошибка просто пропадает — приложение продолжает работать, но данные не загружаются, и отладка становится сложнее.

Правильный вариант: логирование и повторная обработка

			async function fetchData() {
    try {
        const response = await fetch("https://api.example.com/data");
        if (!response.ok) {
            throw new Error(`Ошибка запроса: ${response.status}`);
        }
        return await response.json();
    } catch (error) {
        console.error("Ошибка при запросе данных:", error.message);
        throw error; // Передаём ошибку дальше
    }
}

		

Запоминаем:

  • Не подавляйте ошибки, если они могут повлиять на логику приложения;
  • Логируйте ошибки и передавайте их на следующий уровень обработки, если они критичны;
  • Используйте автоматический перезапрос при сбоях, если это возможно (например, с помощью retry-механизмов).

Обработка ошибок в Node.js — не просто техническая деталь, а критически важная часть разработки. Проблемы могут возникнуть в любом месте кода: от синхронных операций до асинхронных вызовов, API-запросов и работы с базами данных. Следование простым правилам поможет разработчику (будь то джун или сеньор) писать более устойчивый код, избегать критичных багов и создавать по-настоящему надёжные приложения в Node.js.

Следите за новыми постами
Следите за новыми постами по любимым темам
272 открытий2К показов