Продвинутая обработка ошибок в JavaScript: от try/catch до глобальных перехватчиков
Обработка ошибок — это не просто «обёртывание кода в try/catch». В реальных продакшн-приложениях на Node.js грамотное управление исключениями позволяет избежать падений сервера, корректно логировать проблемы и давать пользователю понятные сообщения. В этой статье мы разберём продвинутые техники обработки ошибок в JavaScript, которые выходят за рамки базового синтаксиса.
1. Глубокое погружение в try/catch и finally
Классическая конструкция try...catch...finally остаётся основой синхронной обработки. Однако на продвинутом уровне важно понимать нюансы её работы.
function riskyOperation(value) {
if (typeof value !== 'number') {
throw new TypeError('Argument must be a number');
}
if (value < 0) {
throw new RangeError('Value cannot be negative');
}
return Math.sqrt(value);
}
try {
const result = riskyOperation(-4);
console.log('Result:', result);
} catch (error) {
if (error instanceof TypeError) {
console.error('Type Error:', error.message);
} else if (error instanceof RangeError) {
console.error('Range Error:', error.message);
} else {
console.error('Unknown error:', error);
}
} finally {
console.log('Cleanup: always executed');
}
Ключевые моменты:
- Блок
finallyвыполняется всегда, даже если вtryилиcatchестьreturn. - Используйте встроенные классы ошибок (
TypeError,RangeError,SyntaxError) для точной идентификации. - Не перехватывайте ошибки, которые вы не можете обработать — дайте им всплыть выше.
2. Кастомные классы ошибок и расширение Error
Для крупных проектов стандартных типов ошибок недостаточно. Создание собственных классов позволяет добавлять метаданные (HTTP-статус, код ошибки, контекст) и упрощает фильтрацию.
class AppError extends Error {
constructor(message, statusCode, code) {
super(message);
this.name = 'AppError';
this.statusCode = statusCode;
this.code = code;
Error.captureStackTrace(this, this.constructor);
}
}
class ValidationError extends AppError {
constructor(message, field) {
super(message, 400, 'VALIDATION_ERROR');
this.field = field;
}
}
class DatabaseError extends AppError {
constructor(message, query) {
super(message, 500, 'DB_ERROR');
this.query = query;
}
}
// Использование
try {
throw new ValidationError('Email is required', 'email');
} catch (error) {
if (error instanceof ValidationError) {
console.log(`Field: ${error.field}, Status: ${error.statusCode}`);
}
}
Почему это важно: кастомные ошибки позволяют централизованно обрабатывать разные сценарии (например, в middleware Express.js) и возвращать клиенту структурированный JSON.
3. Обработка ошибок в асинхронном коде: async/await и Promise
В Node.js большая часть кода асинхронна. Ошибки в Promise и async/await требуют особого подхода.
3.1. Обработка Promise
function fetchUserData(userId) {
return new Promise((resolve, reject) => {
if (userId <= 0) {
reject(new RangeError('Invalid user ID'));
}
// имитация запроса к БД
setTimeout(() => resolve({ id: userId, name: 'Alice' }), 100);
});
}
fetchUserData(-1)
.then(user => console.log(user))
.catch(error => {
console.error('Promise rejected:', error.message);
})
.finally(() => console.log('Cleanup after promise'));
3.2. Обработка async/await
async function getUserProfile(userId) {
try {
const user = await fetchUserData(userId);
const posts = await fetchUserPosts(user.id);
return { user, posts };
} catch (error) {
// Логируем и пробрасываем дальше
console.error('Failed to get user profile:', error);
throw new AppError('Unable to load profile', 500, 'PROFILE_ERROR');
}
}
// Использование с обработкой на верхнем уровне
(async () => {
try {
const profile = await getUserProfile(1);
console.log(profile);
} catch (error) {
console.error('Top-level error:', error.message);
}
})();
Совет: не используйте try/catch вокруг каждого await. Лучше группировать логически связанные операции в одном блоке и пробрасывать ошибки вверх.
4. Глобальные обработчики ошибок в Node.js
Даже при тщательной обработке, некоторые ошибки могут остаться неперехваченными. В Node.js существуют специальные события для их перехвата.
// Неперехваченные исключения (uncaughtException)
process.on('uncaughtException', (error, origin) => {
console.error('UNCAUGHT EXCEPTION:', error);
console.error('Origin:', origin);
// Важно: завершить процесс после логирования
process.exit(1);
});
// Необработанные отклонения Promise (unhandledRejection)
process.on('unhandledRejection', (reason, promise) => {
console.error('UNHANDLED REJECTION:', reason);
// В Node.js 15+ это приведёт к завершению процесса
});
// Пример, который вызовет unhandledRejection
setTimeout(() => {
Promise.reject(new Error('This error is not handled'));
}, 100);
Важно: uncaughtException — это крайняя мера. После его срабатывания приложение может находиться в нестабильном состоянии. Рекомендуется логировать ошибку и завершать процесс, используя менеджер процессов (PM2, Docker), который перезапустит приложение.
5. Продвинутые паттерны обработки
5.1. Функция-обёртка для async middleware
// Для Express.js
const asyncHandler = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
// Использование
app.get('/users/:id', asyncHandler(async (req, res) => {
const user = await db.findUser(req.params.id);
if (!user) {
throw new AppError('User not found', 404, 'NOT_FOUND');
}
res.json(user);
}));
5.2. Централизованный обработчик ошибок
// Middleware ошибок в Express
app.use((err, req, res, next) => {
const status