В современных веб-приложениях на базе JavaScript, особенно при использовании серверных компонентов, обработка ошибок становится критически важной для обеспечения стабильности, безопасности и предсказуемого поведения системы. Серверные компоненты работают в среде Node.js или аналогичных платформах, где ошибки могут возникать как на уровне асинхронного кода, так и при взаимодействии с внешними сервисами.
Синхронные ошибки возникают в момент выполнения кода
и обычно выбрасываются через throw. Пример синхронной
ошибки:
function parseData(data) {
if (!data) throw new Error("Данные отсутствуют");
return JSON.parse(data);
}
Асинхронные ошибки возникают в промисах,
async/await или при работе с сетевыми запросами. Их
обработка требует использования .catch или конструкции
try/catch внутри асинхронных функций:
async function fetchData(url) {
try {
const response = await fetch(url);
if (!response.ok) throw new Error("Ошибка сети");
return await response.json();
} catch (err) {
console.error("Ошибка при получении данных:", err);
throw err;
}
}
ValidationError,
DatabaseError, NetworkError.class DatabaseError extends Error {
constructor(message) {
super(message);
this.name = "DatabaseError";
}
}
Для серверных компонентов критично иметь глобальный обработчик ошибок, который ловит все необработанные исключения и промисы:
process.on("uncaughtException", (err) => {
console.error("Необработанное исключение:", err);
process.exit(1);
});
process.on("unhandledRejection", (reason, promise) => {
console.error("Необработанный промис:", promise, "Причина:", reason);
});
Эти обработчики позволяют регистрировать ошибки и проводить корректное завершение работы сервера, предотвращая неконтролируемые состояния приложения.
При работе с серверными компонентами ошибки часто связаны с базой данных. Возможные сценарии:
Для каждой ситуации важно создавать читаемые сообщения и корректно
освобождать ресурсы. Пример с использованием
try/catch/finally:
async function saveUser(user) {
let client;
try {
client = await db.connect();
await client.query("INSERT INTO users(name) VALUES($1)", [user.name]);
} catch (err) {
console.error("Ошибка при сохранении пользователя:", err);
throw new DatabaseError("Не удалось сохранить пользователя");
} finally {
if (client) client.release();
}
}
Эффективное логирование разделяет ошибки по уровням: информационные,
предупреждения, критические. Использование библиотек, таких как
winston или pino, позволяет централизованно
хранить логи, добавлять метаданные и интегрироваться с системами
мониторинга.
import pino from "pino";
const logger = pino({ level: "info" });
try {
await fetchData("/api/data");
} catch (err) {
logger.error({ err, context: "fetchData" }, "Ошибка при запросе данных");
}
В серверных компонентах, обрабатывающих HTTP-запросы, ошибки необходимо переводить в корректные ответы клиенту. Для этого используют статус-коды и структурированные сообщения:
app.get("/user/:id", async (req, res) => {
try {
const user = await getUser(req.params.id);
if (!user) return res.status(404).json({ error: "Пользователь не найден" });
res.json(user);
} catch (err) {
res.status(500).json({ error: "Внутренняя ошибка сервера" });
}
});
Для критичных операций, таких как сетевые запросы или обращения к базе данных, полезно использовать стратегии повторных попыток с экспоненциальной задержкой. Это снижает вероятность сбоев из-за временных проблем:
async function retry(fn, retries = 3, delay = 1000) {
for (let i = 0; i < retries; i++) {
try {
return await fn();
} catch (err) {
if (i === retries - 1) throw err;
await new Promise(r => setTimeout(r, delay * Math.pow(2, i)));
}
}
}
Для предотвращения ошибок на ранних стадиях важно использовать
проверку данных, схемы и типизацию. Например, интеграция с библиотеками
вроде zod или joi позволяет создавать строгие
контракты на входящие данные и выбрасывать ошибки в случае
нарушения.
import { z } from "zod";
const userSchema = z.object({
name: z.string().min(1),
email: z.string().email()
});
function validateUser(data) {
try {
return userSchema.parse(data);
} catch (err) {
throw new ValidationError("Некорректные данные пользователя");
}
}
В современных серверных фреймворках, таких как Express, Next.js или Fastify, ошибки в асинхронных функциях должны быть правильно переданы в middleware обработки ошибок. В Express это выглядит так:
app.use(async (req, res, next) => {
try {
await someAsyncOperation();
next();
} catch (err) {
next(err);
}
});
app.use((err, req, res, next) => {
res.status(500).json({ error: err.message });
});
Эффективная обработка ошибок в серверных компонентах позволяет минимизировать сбои, обеспечивать прозрачность работы системы и создавать стабильные веб-приложения, готовые к масштабированию и интеграции с внешними сервисами.