Senior: питання на співбесіді з теми «Помилки й налагодження»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
3 питання
Помилки фронтенду відбуваються в браузерах користувачів, а не на вашому сервері: якщо їх не збирати, про них дізнаються зі скарг (або не дізнаються взагалі).
Джерела помилок:
windowподіяerror- необроблені винятки, а також помилки завантаження ресурсів (зображення, скрипти) при підписці зcapture: true;unhandledrejection- необроблені відхилення Promise;- явні виклики
report(error)уcatch, де помилку обробили, але про неї треба знати; - обробники помилок фреймворків:
app.config.errorHandlerу Vue, error boundaries у React - вони ловлять помилки рендеру, які інакше лише покажуть порожній екран.
Готові рішення - Sentry, Bugsnag, Flare (від авторів Ignition, з інтеграцією з Laravel): SDK підписується на всі ці події, групує однакові помилки й показує частоту.
Що робить звіт корисним:
- читабельний стек - з source maps, завантаженими в систему моніторингу при деплої (самі карти на сервер не публікують);
- версія релізу - щоб розуміти, в якому деплої з'явилася помилка і чи виправив її новий;
- «хлібні крихти» (breadcrumbs) - що відбувалося перед помилкою: кліки, переходи, мережеві запити, повідомлення консолі. Найчастіше саме вони пояснюють, як відтворити проблему;
- контекст: сторінка, браузер, ОС, користувач (ідентифікатор, а не персональні дані);
- зв'язок з бекендом - ідентифікатор запиту (trace id), щоб знайти відповідний запис у логах Laravel.
Шум, який треба фільтрувати:
- помилки розширень браузера й вбудованих скриптів (стек вказує на
chrome-extension://); Script error.без деталей - скрипти з інших доменів безcrossorigin;ResizeObserver loop completed with undelivered notifications- зазвичай нешкідливе попередження;- помилки мережі при закритті вкладки,
AbortErrorвід свідомо скасованих запитів; - помилки завантаження частин після деплою (
Failed to fetch dynamically imported module): старі вкладки просять файли, яких уже немає. Це треба обробляти (запропонувати оновити сторінку), а не лише рахувати.
Обсяг і ціна:
- вибірка (sampling) - на великому трафіку надсилати частину однакових помилок, а не всі;
- обмеження частоти з одного браузера - одна помилка в циклі рендеру може згенерувати тисячі звітів за хвилину;
- конфіденційність: не відправляти вміст полів форм, токени, повні URL з персональними даними в параметрах.
Метрика, на яку дивитися: не загальна кількість помилок, а кількість користувачів, які зіткнулися з кожною помилкою, і помилки, що з'явилися в останньому релізі.
Блок finally виконується після try і catch у будь-якому разі. Але якщо він сам завершується через return, throw, break чи continue, це замінює результат усього try...catch.
return у finally ковтає виняток:
function save() {
try {
throw new Error('Диск заповнено');
} finally {
return 'ok';
}
}
save(); // 'ok' - помилка зникла без сліду
І перекриває return з try:
function getStatus() {
try {
return 'from try';
} finally {
return 'from finally';
}
}
getStatus(); // 'from finally'
throw у finally замінює початкову помилку:
try {
throw new Error('справжня причина');
} finally {
cleanup(); // якщо cleanup() кине свою помилку, «справжня причина» загубиться
}
Це особливо підступно: у логах буде помилка прибирання («Cannot read properties of null»), а справжня проблема - прихована.
Що відбувається насправді. Коли try завершується (return, виняток), результат «запам'ятовується», виконується finally, і лише потім запам'ятований результат застосовується. Але якщо finally сам завершився «різко», запам'ятоване відкидається.
Ще одна тонкість - значення return обчислюється до finally:
function f() {
let x = 1;
try {
return x; // повернеться 1
} finally {
x = 2; // змінна змінилася, але значення вже обчислено
}
}
З об'єктом інакше: повертається посилання, і зміна властивостей у finally буде видна.
Правила для finally:
- лише прибирання: закрити, зняти блокування, приховати індикатор;
- ніколи не
return,break,continue- лінтерno-unsafe-finallyловить це автоматично; - прибирання, що саме може впасти, загортати у власний
try...catch, щоб не перекрити основну помилку:
} finally {
try {
await connection.close();
} catch (closeError) {
report(closeError); // повідомити, але не перекрити помилку з try
}
}
Сучасна альтернатива для гарантованого прибирання ресурсів - using з явним керуванням ресурсами (Symbol.dispose): ресурс звільняється автоматично при виході з блоку. Він уже є в TypeScript і поступово з'являється в рушіях JavaScript.
Аналогія з PHP: там return у finally теж перекриває і return, і виняток з try - правило однакове для обох мов.
Найчастіша проблема з помилками - не відсутність обробки, а обробка не в тому місці: try...catch у кожній функції, де помилку логують і повертають null, а вищий рівень так і не дізнається, що щось пішло не так.
Принцип: ловити там, де знаєш, що робити.
Функція, яка не може осмислено відреагувати на помилку, має пропустити її далі. «Що робити» майже завжди вирішує рівень, близький до користувача.
Шари типового застосунку:
1. Низький рівень (HTTP-клієнт, парсери) - перетворює помилки на зрозумілі типи, але не ховає їх:
async function apiRequest(url, options) {
const response = await fetch(url, options);
if (response.status === 422) throw new ValidationError((await response.json()).errors);
if (response.status === 401) throw new UnauthenticatedError();
if (!response.ok) throw new HttpError(response);
return response.json();
}
2. Сервіси й бізнес-логіка - зазвичай не ловлять нічого. Винятки - очікувані ситуації з альтернативною поведінкою: кеш недоступний - читаємо напряму; необов'язкова рекомендація не завантажилася - показуємо сторінку без неї.
3. Межа взаємодії (обробник форми, дія користувача) - тут вирішують, що показати:
async function onSubmit() {
try {
await saveOrder(form);
showToast('Замовлення збережено');
} catch (error) {
if (error instanceof ValidationError) return showFieldErrors(error.errors);
if (error instanceof UnauthenticatedError) return redirectToLogin();
report(error); // неочікуване - у моніторинг
showToast('Не вдалося зберегти. Спробуйте ще раз');
}
}
4. Глобальний рубіж - window.onerror, unhandledrejection, errorHandler фреймворку, error boundary: для того, що проскочило, - звіт у моніторинг і запасний інтерфейс замість білого екрана.
Очікувані й неочікувані помилки:
- очікувані (валідація, 404 «товар не знайдено», немає прав) - частина звичайного сценарію. Їх обробляють і не надсилають у моніторинг як помилки;
- неочікувані (
TypeError, 500) - баги. Їх показують користувачеві загальним повідомленням і обов'язково звітують.
Змішування призводить до того, що моніторинг завалений тисячами «помилок валідації», а справжній баг тоне в шумі.
Антипатерни:
catch (e) { console.log(e) }без подальшої дії - помилка «оброблена», але застосунок у зламаному стані;- повернення
null/falseзамість винятку - виклики далі мусять перевіряти результат, і рано чи пізно перевірку забудуть; - однакове «Щось пішло не так» для всього - і для помилки валідації, і для недоступного сервера;
catchлише для того, щобthrowту саму помилку - шум.
Альтернатива винятками - тип-результат ({ ok: true, value } / { ok: false, error }), популярний у TypeScript для очікуваних помилок: компілятор змушує обробити обидва варіанти. Але для неочікуваних помилок винятки лишаються природнішими.