Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

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 з персональними даними в параметрах.

Метрика, на яку дивитися: не загальна кількість помилок, а кількість користувачів, які зіткнулися з кожною помилкою, і помилки, що з'явилися в останньому релізі.

Докладніше в документації: Подія error

Блок 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: блок finally

Найчастіша проблема з помилками - не відсутність обробки, а обробка не в тому місці: 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 для очікуваних помилок: компілятор змушує обробити обидва варіанти. Але для неочікуваних помилок винятки лишаються природнішими.

Докладніше в документації: Error