Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Обробка помилок: try/catch/finally в JavaScript». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**`try/catch/finally`** - вбудований механізм JavaScript для обробки помилок під час виконання. Блок `try` запускає код який може впасти, `catch` обробляє кинуту помилку, `finally` виконує очищення незалежно від результату. ```javascript try { const data = JSON.parse('bad json'); // Кидає SyntaxError } catch (error) { console.log(error.message); // "Unexpected token..." } finally { console.log('Завжди виконується'); // Навіть після catch } ``` **Ключове:** `finally` спрацьовує навіть якщо `catch` робить ранній `return` або повторний `throw`, тому він підходить для закриття з'єднань і зупинки завантажень.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**`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` завжди завершується перед тим як помилка продовжить поширення назовні.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.