Обробка помилок: try/catch/finally в JavaScript
try/catch/finally - конструкція JavaScript, яка дозволяє спробувати виконати ризикований код, перехопити будь-яку кинуту помилку та запустити код очищення незалежно від результату.
Теорія
TL;DR
- Аналогія: хірург з планом Б.
try- це операція,catch- реакція на ускладнення,finally- зашивання в будь-якому випадку finallyзавжди виконується, навіть якщоcatchповторно кидає помилку абоreturnвиходить раніше- Загортай API-запити,
JSON.parse, читання файлів: все що може впасти в runtime - Ніколи не використовуй
try/catchдля управління потоком. Для цього єif/else
Швидкий приклад
try {
const data = JSON.parse('{ invalid }'); // Кидає SyntaxError
console.log(data); // Пропускається
} catch (error) {
console.log('Caught:', error.message); // "Unexpected token..."
} finally {
console.log('Cleanup done'); // Завжди виводиться
}
// Output:
// Caught: Unexpected token i in JSON at position 2
// Cleanup doneКоли JSON.parse кидає помилку, виконання одразу переходить до catch. Код після throw всередині try пропускається. Потім виконується finally.
Що насправді робить finally
finally виконується після завершення try/catch незалежно від результату: успіх, перехоплена помилка або повторний throw. Без нього довелося б дублювати код очищення і в try-гілці, і в catch-гілці. Один блок покриває обидва випадки.
Найбільше це важливо при звільненні ресурсів: закриття з'єднань з базою, зупинка спінерів завантаження, звільнення файлових дескрипторів. Якщо catch повертає значення раніше, finally все одно спрацює перед фактичним виходом з функції.
Коли використовувати
- API-запит може впасти через мережу або поганий статус-код: загорни в
try/catch, покажи зрозуміле повідомлення JSON.parseна введених даних: перехопиSyntaxError, поверни значення за замовчуванням- Читання файлів у Node.js:
finallyзакриє потік навіть якщо читання провалилось async/awaitфункції:try/catch- це чистий спосіб обробляти відхилені Promise-и- Пропускай для передбачуваних шляхів. Перевіряй умови через
if/else
Як V8 це обробляє
Під час компіляції V8 загортає блок try в обробник виключень і будує внутрішню таблицю виключень. Коли відбувається throw, рушій розгортає стек викликів і переходить до catch. Після виконання catch запускається finally в тій самій лексичній області видимості. У браузерах необроблені помилки також запускають window.onerror. У Node.js вони генерують подію uncaughtException.
Типові помилки
1. Думати що finally пропускається при return
function test() {
try {
return 'success';
} finally {
console.log('Це виконається перед поверненням');
}
}
console.log(test());
// "Це виконається перед поверненням"
// "success"finally спрацьовує до фактичного повернення. Але якщо finally має власний return, він перезаписує значення з блоку try. Використовуй finally тільки для побічних ефектів, не для повернення значень.
2. Мовчазне проковтування всіх помилок
// Погано: ховає справжні баги
try {
JSON.parse('bad');
} catch (e) {}
// Краще: перевіряй тип
try {
JSON.parse('bad');
} catch (error) {
if (error instanceof SyntaxError) {
console.warn('Невалідний JSON, використовую defaults');
} else {
throw error; // Перекидаємо неочікувані помилки
}
}TypeError від помилки в коді виглядає так само, як SyntaxError від поганих даних. Catch-all блоки ховають обидва однаково добре.
3. Забувати finally для звільнення ресурсів у Node.js
// Ризик: потік залишається відкритим
const stream = fs.createReadStream('file.txt');
try {
// читаємо дані
} catch (error) {
console.error(error);
}
// Правильно
try {
// читаємо дані
} catch (error) {
console.error(error);
} finally {
stream.destroy(); // Виконується в будь-якому випадку
}Найпоширеніша проблема яку я бачив у Node.js кодових базах - саме цей патерн в обробниках маршрутів з базою даних. Один провальний запит залишає з'єднання відкритим. Достатньо кількох таких - і пул вичерпується повністю.
4. Використання try/catch для управління потоком
// Повільно і незрозуміло
try {
riskyOperation();
} catch {
fallback();
}
// Якщо можна перевірити заздалегідь - перевіряй
if (canRun()) {
riskyOperation();
} else {
fallback();
}Виключення несуть накладні витрати у V8. Використовуй їх для справді неочікуваних збоїв, а не для передбачуваних гілок.
Де зустрічається в реальних проектах
- Express.js: асинхронні обробники маршрутів загорнуті в
try/catch, невідомі помилки передаються доnext(error)для глобального middleware - React:
useEffectз fetch загорнутий уtry/catch, очищення черезAbortControllerуfinally - Node.js fs/promises:
readFileзfinallyдля закриття потоків - Axios interceptors:
try/catchвсередині трансформерів запитів і відповідей
Питання на співбесіді
Q: Що відбудеться якщо finally має return?
A: Він перезаписує значення повернення з try або catch. Попереднє значення відкидається. Це часто дивує авторів утилітарних функцій.
Q: Чи перехоплює try/catch помилки в асинхронних колбеках типу setTimeout?
A: Ні. try/catch навколо setTimeout(() => { throw new Error() }) не спрацює, бо колбек виконується в іншому контексті виконання. Постав try/catch всередині колбека або переходь на async/await.
Q: Чи можна писати try/finally без catch?
A: Так. Помилка все одно поширюється вгору по стеку, але finally виконається першим. Корисно коли потрібне очищення, але не потрібно обробляти помилку на цьому рівні.
Q: Яка різниця між .catch() на Promise і try/catch в async функції?
A: Обидва перехоплюють відхилені Promise-и. try/catch в async функції зазвичай читабельніший для послідовного асинхронного коду. .catch() краще підходить для inline-обробки в ланцюжках промісів.
Q: (Senior) Як re-throw взаємодіє з finally?
A: Повторний throw в catch не пропускає finally. Розгортання стека завершується тільки після того як finally відпрацює. Тобто finally завжди виконується, і потім помилка продовжує поширюватися у зовнішній контекст.
Приклади
Безпечний парсинг JSON із fallback
function parseConfig(input) {
try {
const config = JSON.parse(input); // Може кинути SyntaxError
return config;
} catch (error) {
if (error instanceof SyntaxError) {
console.warn('Невалідний формат, використовую defaults');
return { theme: 'light', lang: 'en' }; // Значення за замовчуванням
}
throw error; // Перекидаємо не-синтаксичні помилки
}
}
console.log(parseConfig('{ "theme": "dark" }')); // { theme: "dark" }
console.log(parseConfig('not json')); // { theme: "light", lang: "en" }Функція обробляє SyntaxError окремо і повертає дефолт. Все інше перекидається, щоб справжні баги не зникали непомітно.
Express-маршрут з очищенням з'єднання з базою
app.get('/user/:id', async (req, res, next) => {
try {
const user = await db.getUser(req.params.id); // Падає на невалідному ID
res.json(user);
} catch (error) {
if (error.name === 'NotFoundError') {
res.status(404).json({ error: 'User not found' });
} else {
next(error); // Передаємо глобальному обробнику Express
}
} finally {
await db.releaseConnection(); // Завжди звільняємо connection pool
}
});Навіть якщо db.getUser кидає помилку або викликається next(error), з'єднання звільняється. Без finally провальний запит витікав би з'єднанням з пулу.
Поведінка re-throw з async/await
async function risky() {
try {
throw new Error('Boom');
} catch (error) {
console.log('Caught:', error.message); // "Boom"
throw error; // Перекидаємо
} finally {
console.log('Finally runs'); // Виконується навіть при re-throw
}
}
risky().catch(e => console.log('Outer:', e.message));
// Output:
// Caught: Boom
// Finally runs
// Outer: BoomБагато розробників думають, що re-throw пропускає finally. Ні. finally завжди завершується перед тим як помилка продовжить поширення назовні.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.