Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Обробка помилок у Vue.js». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Обробка помилок у Vue.js** перехоплює виключення в компонентах до того, як вони обваляться. ```vue onErrorCaptured((err, instance, info) => { error.value = err.message // info: "render", "setup()", "watcher" return false // зупиняємо спливання }) ``` Використовуй `onErrorCaptured` у батьківському компоненті для перехоплення синхронних помилок нащадків. Для async fetch - `try/catch`. Для глобального логування в Sentry - `app.config.errorHandler` у `main.ts`. **Головне:** `onErrorCaptured` перехоплює лише синхронні throws. Async rejection потребує `try/catch`.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Обробка помилок у Vue.js** перехоплює виключення в компонентах і не дає їм обвалити весь застосунок. ## Теорія ### TL;DR - `onErrorCaptured` у батьківському компоненті перехоплює синхронні помилки будь-якого нащадка під час render, setup або watch - Як протипожежна смуга: помилка доходить до межі і далі не поширюється - Повернення `false` з хука зупиняє спливання вгору по дереву компонентів - Лише синхронні throws. Async-функції та Promise rejection потребують `try/catch` - `app.config.errorHandler` - глобальний запасний обробник для всього, що пропустили локальні межі ### Швидкий приклад ```vue <!-- ErrorBoundary.vue --> <script setup> import { ref, onErrorCaptured } from 'vue' const hasError = ref(false) const errorMsg = ref('') onErrorCaptured((err, instance, info) => { hasError.value = true errorMsg.value = err.message // наприклад "Failed to parse JSON" // info вказує де: "render", "setup()", "watcher" return false // зупиняємо спливання }) </script> <template> <div v-if="hasError" class="error-state"> {{ errorMsg }} <button @click="hasError = false">Повторити</button> </div> <slot v-else /> <!-- дочірній компонент падає тут, помилка перехоплюється --> </template> ``` Обгорни будь-який нестабільний дочірній компонент цією межею. Коли він кидає помилку, слот перемикається на стан помилки. Решта сторінки залишається. ### Як поширюються помилки Vue обробляє помилки компонентів подібно до DOM-подій: вони спливають вгору по дереву. Синхронна помилка в `setup()` або `render` онука піднімається до найближчого предка з `onErrorCaptured`. Хук отримує три аргументи: об'єкт помилки, екземпляр компонента і рядок `info` на кшталт `"render"` або `"setup()"`. Якщо хук повертає `false`, спливання зупиняється. Якщо ні, помилка продовжує підніматися і зрештою доходить до `app.config.errorHandler`. Цей глобальний обробник спрацьовує останнім, для всього що локальні межі не зупинили. Параметр `info` варто логувати. `"setup()"` означає, що компонент впав під час ініціалізації. `"render"` - під час обчислення шаблону. Без цього контексту дебагінг займає більше часу. ### Коли що використовувати - Ізолювати один нестабільний віджет від решти UI: `onErrorCaptured` у компоненті-обгортці навколо нього - Логувати все в Sentry або Datadog: `app.config.errorHandler` у `main.ts` - Fetch повертає помилку всередині компонента: `try/catch` навколо `await` - Необроблені Promise rejection поза Vue: `window.addEventListener('unhandledrejection', ...)` - Попередження тільки на dev: `app.config.warnHandler`, прибрати з production білда `onErrorCaptured` не підходить для async-коду. Це перша помилка, на яку натрапляє більшість розробників. ### Як це працює всередині Коли синхронна помилка виникає в setup або render-циклі дочірнього компонента, Vue перехоплює її ще до того, як вона може зіпсувати стан батька. Невдале піддерево пропускається, і керування передається системі обробки помилок, яка піднімається через `onErrorCaptured`-хуки. DOM батька залишається примонтованим. У Vue 3.4+, помилки async setup всередині `<Suspense>` передаються найближчій межі, а не обходять її. ### Типові помилки **1. Очікування, що `onErrorCaptured` перехоплює async rejection.** ```vue <script setup> async function loadData() { throw new Error('fetch failed') } loadData() // Promise відхиляється, onErrorCaptured не спрацює </script> ``` Рішення: обгортай async-виклики в `try/catch` і вручну оновлюй реактивний ref з помилкою. **2. Забутий `return false`.** ```vue onErrorCaptured((err) => { console.log(err) // логує, але помилка все одно спливає далі }) ``` Без `return false` помилка доходить до `app.config.errorHandler` і консолі браузера. Додавай, якщо хочеш утримати помилку саме в цій межі. **3. Межа в тому самому компоненті, що кидає помилку.** `onErrorCaptured` перехоплює лише помилки нащадків, не власні. Якщо компонент кидає у своєму `setup()`, хук у цьому ж компоненті не спрацює. Використовуй окрему обгортку `ErrorBoundary.vue`. **4. Немає механізму повтору.** Просто очистити error ref недостатньо, якщо в дочірньому компоненті залишився поганий стан. При повторі скидай дані, що спричинили падіння, а потім перемонтовуй дочірній компонент. Кнопка "Спробувати ще", що лише очищає ref, часто одразу перезапускає ту саму помилку. **5. Ігнорування параметра `info`.** Рядки `"render"`, `"setup()"`, `"watcher"` точно вказують на фазу lifecycle. Логування тільки `err.message` губить цей контекст. Передавай `info` у Sentry як додаткові дані. ### Де зустрічається в реальних проектах - Nuxt.js: встановлює `app.config.errorHandler` у `plugins/error-handler.client.ts` для виявлення SSR mismatch. Потрібен guard `process.client`. - Quasar Framework: обгортає layout-компоненти межами з auto-retry. - VueUse: composable-и на кшталт `useFetch` повертають реактивний `error` ref, тому незловлені виключення не потрапляють до коду компонента. - Sentry: передавай `err` до `Sentry.captureException(err)` всередині `app.config.errorHandler`, додавай `info` і ім'я компонента як extra. ### Питання на співбесіді **Q:** Які помилки перехоплює `onErrorCaptured`? **A:** Синхронні помилки в `setup()`, `render`, watchers і lifecycle hooks нащадків. Не перехоплює помилки з emit, async-функцій без await у відстежуваному контексті, і помилки в `setTimeout`. **Q:** У чому різниця між `onErrorCaptured` і `app.config.errorHandler`? **A:** `onErrorCaptured` локальний і спрацьовує першим. `app.config.errorHandler` глобальний і спрацьовує останнім. Використовуй обидва: межі для ізоляції UI, глобальний обробник для логування. **Q:** Як `onErrorCaptured` взаємодіє з `<Suspense>`? **A:** У Vue 3.4+, помилки async setup у `<Suspense>` передаються найближчій межі. На старших версіях вони могли її оминати. Для надійності обгортай async setup у `try/catch`. **Q:** Чому межа помилок може не спрацювати в Nuxt SSR? **A:** Хук запускається на клієнті. Під час server rendering він може не спрацювати. Використовуй guard `process.client` у Nuxt-плагінах і обробляй серверні помилки в server middleware. **Q:** (Senior) Як побудувати межу помилок з відновленням стану Pinia при повторі? **A:** У handler повтору виклич `store.$reset()` до очищення error ref. Потім використай `nextTick`, щоб дочірній компонент перемонтувався з чистим станом. Без `nextTick` компонент може перерендеритись до завершення скидання стору. ## Приклади ### Базова межа помилок ```vue <!-- ErrorBoundary.vue --> <script setup> import { ref, onErrorCaptured } from 'vue' const error = ref(null) onErrorCaptured((err, instance, info) => { error.value = { message: err.message, info } return false }) </script> <template> <div v-if="error" class="fallback"> <p>{{ error.message }} (у {{ error.info }})</p> <button @click="error = null">Повторити</button> </div> <slot v-else /> </template> ``` ```vue <!-- App.vue --> <template> <ErrorBoundary> <UserDashboard /> </ErrorBoundary> </template> ``` Коли `UserDashboard` кидає помилку під час render або setup, межа перехоплює її і показує fallback. Клік на "Повторити" очищає ref і перерендеровує слот. ### Віджет дашборду з проблемними API-даними ```vue <!-- DashboardWidget.vue --> <script setup> import { ref, onErrorCaptured } from 'vue' import UserChart from './UserChart.vue' const error = ref(null) onErrorCaptured((err) => { error.value = err.message // наприклад "Failed to parse JSON" return false }) function retry() { error.value = null } </script> <template> <div class="dashboard-card"> <h3>Метрики користувачів</h3> <div v-if="error" class="error-fallback"> {{ error }} <button @click="retry">Оновити дані</button> </div> <UserChart v-else /> </div> </template> ``` `UserChart` парсить дані API під час setup. Якщо API повертає некоректний JSON, парсинг кидає синхронну помилку, межа перехоплює її і показує кнопку оновлення. Решта дашборду продовжує нормально відображатися. ### Глобальний обробник з Sentry ```typescript // main.ts import { createApp } from 'vue' import * as Sentry from '@sentry/vue' import App from './App.vue' const app = createApp(App) app.config.errorHandler = (err, instance, info) => { Sentry.captureException(err, { extra: { info, component: instance?.$options?.name ?? 'unknown', }, }) console.error('[global error]', err) } app.mount('#app') ``` Пеrehоплює все, що не зупинили локальні межі. Рядок `info` і ім'я компонента допомагають знайти проблемну частину застосунку без копання в стек трейсах.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.