Skip to main content

Обробка помилок: try/catch/finally в JavaScript

try/catch/finally - конструкція JavaScript, яка дозволяє спробувати виконати ризикований код, перехопити будь-яку кинуту помилку та запустити код очищення незалежно від результату.

Теорія

TL;DR

  • Аналогія: хірург з планом Б. try - це операція, catch - реакція на ускладнення, finally - зашивання в будь-якому випадку
  • finally завжди виконується, навіть якщо catch повторно кидає помилку або return виходить раніше
  • Загортай API-запити, JSON.parse, читання файлів: все що може впасти в runtime
  • Ніколи не використовуй try/catch для управління потоком. Для цього є if/else

Швидкий приклад

javascript
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

javascript
function test() { try { return 'success'; } finally { console.log('Це виконається перед поверненням'); } } console.log(test()); // "Це виконається перед поверненням" // "success"

finally спрацьовує до фактичного повернення. Але якщо finally має власний return, він перезаписує значення з блоку try. Використовуй finally тільки для побічних ефектів, не для повернення значень.

2. Мовчазне проковтування всіх помилок

javascript
// Погано: ховає справжні баги 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

javascript
// Ризик: потік залишається відкритим 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 для управління потоком

javascript
// Повільно і незрозуміло 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

javascript
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-маршрут з очищенням з'єднання з базою

javascript
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

javascript
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 завжди завершується перед тим як помилка продовжить поширення назовні.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?