Error handling
Обработка ошибок — механизм перехвата и обработки исключений для предотвращения аварийного завершения программы.
Зачем нужно
Ошибки неизбежны: сеть недоступна, пользователь ввёл невалидные данные, API вернул неожиданный формат. Грамотная обработка ошибок — разница между рабочим приложением и падающим.
Где используется
- Сетевые запросы (fetch, API)
- Парсинг данных (JSON.parse)
- Работа с DOM (элемент не найден)
- Валидация ввода
- Файловые операции
Предпосылки
try/catch/finally
try {
const data = JSON.parse('invalid json');
} catch (error) {
console.error('Ошибка парсинга:', error.message);
} finally {
console.log('Выполнится в любом случае');
}
// catch получает объект ошибки
try {
undeclaredVar;
} catch (error) {
console.log(error.name); // "ReferenceError"
console.log(error.message); // "undeclaredVar is not defined"
console.log(error.stack); // полный стек вызовов
}
// catch без переменной (ES2019)
try {
riskyOperation;
} catch {
// Ошибка не нужна — просто обработали
fallbackOperation;
}
Типы встроенных ошибок
// Error — базовый тип
new Error('Что-то пошло не так');
// TypeError — операция с неправильным типом
null.toString(); // TypeError
undefined.prop; // TypeError
// ReferenceError — обращение к несуществующей переменной
console.log(notDeclared); // ReferenceError
// SyntaxError — ошибка синтаксиса
JSON.parse('{invalid}'); // SyntaxError
// RangeError — значение вне допустимого диапазона
new Array(-1); // RangeError
(1).toFixed(200); // RangeError
// URIError — неправильный URI
decodeURIComponent('%'); // URIError
// AggregateError (ES2021) — множество ошибок
Promise.any([
Promise.reject(new Error('1')),
Promise.reject(new Error('2'))
]).catch(err => {
console.log(err instanceof AggregateError); // true
console.log(err.errors); // [Error('1'), Error('2')]
});
throw — генерация ошибок
// Бросить ошибку
throw new Error('Пользователь не найден');
// Можно бросить любое значение (но лучше Error)
throw 'строка ошибки'; // Работает, но нет стека
throw 42; // Работает, но бесполезно
throw { code: 404, msg: 'Not found' }; // Нет стека
// Всегда используй new Error или его наследников
throw new TypeError('Ожидалось число, получена строка');
Кастомные ошибки
class AppError extends Error {
constructor(message, code) {
super(message);
this.name = 'AppError';
this.code = code;
}
}
class ValidationError extends AppError {
constructor(field, message) {
super(message, 'VALIDATION_ERROR');
this.name = 'ValidationError';
this.field = field;
}
}
class NotFoundError extends AppError {
constructor(entity, id) {
super(`${entity} с id ${id} не найден`, 'NOT_FOUND');
this.name = 'NotFoundError';
this.entity = entity;
this.entityId = id;
}
}
// Использование
function getUser(id) {
const user = db.find(u => u.id === id);
if (!user) throw new NotFoundError('User', id);
return user;
}
try {
const user = getUser(999);
} catch (error) {
if (error instanceof NotFoundError) {
console.log(`${error.entity} не найден`);
} else if (error instanceof ValidationError) {
console.log(`Ошибка валидации: ${error.field}`);
} else {
throw error; // Неизвестная ошибка — пробрасываем дальше
}
}
Ошибки в асинхронном коде
Promise .catch
fetch('/api/data')
.then(response => response.json())
.then(data => {
throw new Error('Ошибка обработки');
})
.catch(error => {
// Ловит ошибки от ВСЕХ .then выше
console.error(error.message);
});
async/await + try/catch
async function loadData() {
try {
const response = await fetch('/api/data');
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
if (error instanceof TypeError) {
console.error('Сетевая ошибка');
} else {
console.error('Ошибка:', error.message);
}
return null;
}
}
Глобальные обработчики
// Браузер: необработанные ошибки
window.addEventListener('error', (event) => {
console.error('Глобальная ошибка:', event.message);
// Отправить в сервис мониторинга
});
// Браузер: необработанные rejected Promise
window.addEventListener('unhandledrejection', (event) => {
console.error('Необработанный Promise:', event.reason);
event.preventDefault(); // Предотвратить вывод в консоль
});
// Node.js
process.on('uncaughtException', (error) => {
console.error('Uncaught:', error);
process.exit(1); // Рекомендуется завершить процесс
});
process.on('unhandledRejection', (reason) => {
console.error('Unhandled rejection:', reason);
});
Паттерны обработки ошибок
Result-паттерн (без исключений)
function safeParse(json) {
try {
return { ok: true, value: JSON.parse(json) };
} catch (error) {
return { ok: false, error: error.message };
}
}
const result = safeParse('{"a": 1}');
if (result.ok) {
console.log(result.value);
} else {
console.error(result.error);
}
Обёртка для async-функций
function catchAsync(fn) {
return function(...args) {
return fn(...args).catch(error => {
console.error('Async error:', error);
});
};
}
const safeLoad = catchAsync(async (url) => {
const res = await fetch(url);
return res.json();
});
Частые ошибки
1. Поглощение ошибок
// Плохо: ошибка молча проглочена
try {
riskyOperation;
} catch (e) {
// Пустой catch — ошибка потеряна!
}
// Хорошо: как минимум логируй
try {
riskyOperation;
} catch (e) {
console.error(e);
}
2. Ловить слишком широко
// Плохо: ловим ВСЕ ошибки одинаково
try {
doStuff;
} catch (e) {
alert('Ошибка!');
}
// Хорошо: различай типы ошибок
try {
doStuff;
} catch (e) {
if (e instanceof ValidationError) {
showFieldError(e.field);
} else {
throw e; // Пробросить неизвестные ошибки
}
}
Практика
- Создай 3 кастомных класса ошибок с наследованием
- Напиши обёртку для fetch с правильной обработкой всех типов ошибок
- Реализуй глобальный обработчик ошибок с отправкой в «сервис мониторинга»
- Напиши функцию
safeParseдля JSON с Result-паттерном
Связанные темы
Ресурсы
🎓 Источник: Необработанные ошибки в промисах на Node.js
- 📅 2019-03-07 · YouTube
- Тезисы:
unhandledRejection— событие наprocess, срабатывает, когда промис отклонён и.catchне повешен в текущем тике.rejectionHandled— если.catchповесили ПОЗЖЕ (асинхронно). Обычно это симптом ошибки архитектуры.- С Node 15+ unhandled rejection по умолчанию завершает процесс (
--unhandled-rejections=throw). - Лечение: всегда
await+try/catch, либо явный.catch, либоPromise.allSettledдля несвязанных задач. - В браузере —
window.addEventListener('unhandledrejection', e => ...), можноe.preventDefault()чтобы подавить вывод в консоль.
🎓 Источник: Проблема асинхронного стектрейса в JavaScript и Node.js
- 📅 2019-03-21 · YouTube
- Тезисы:
- До Node 12 / V8 7.2 цепочка async-вызовов в stack trace терялась.
- Throw из callback'а в
setTimeout(cb)не ловитсяtry/catchснаружи — он будет unhandled error в event loop. - С
--async-stack-traces(включён по умолчанию в Node 12+) await-цепочка восстанавливается. - Throw из async-функции автоматически становится rejected Promise —
try/catchсawaitловит.
- См. отдельную заметку Async stack trace.