Junior: питання на співбесіді з теми «Помилки й налагодження»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
3 питання
try - блок коду, у якому може статися помилка. catch - що робити, якщо вона сталася. finally - що виконати в будь-якому разі.
function loadSettings() {
try {
const raw = localStorage.getItem('settings');
return JSON.parse(raw) ?? {};
} catch (error) {
console.warn('Пошкоджені налаштування, використовуємо типові', error);
return {};
} finally {
hideLoader(); // виконається і при успіху, і при помилці
}
}
Як це працює:
- коли в
tryкидається виняток, решта блоку пропускається і керування переходить уcatch; - у
catchдоступний об'єкт помилки:error.name,error.message,error.stack; - якщо помилки не було,
catchне виконується; finallyвиконується завжди - навіть якщо вtryчиcatchбувreturnчи нова помилка.
Змінна в catch необов'язкова:
try {
JSON.parse(input);
} catch {
showError('Некоректний JSON');
}
Кинути власну помилку - throw:
if (!email.includes('@')) {
throw new Error('Некоректна електронна адреса');
}
Кидати варто об'єкти Error (або нащадки), а не рядки: throw 'помилка' не має стеку викликів, і налагоджувати таке набагато важче.
Типові помилки новачків:
- ловити все й мовчати:
catch (e) {}- помилка зникла, а програма працює з неправильними даними. Як мінімум - записати в лог; - занадто великий
try: обгорнути всю функцію, а потім не розуміти, який рядок кинув виняток. Краще обгортати лише те, що справді може впасти і що ви знаєте, як обробити; - очікувати, що
tryзловить помилку в колбекуsetTimeoutчи обробнику події - не зловить, бо колбек виконується пізніше, колиtryуже завершено; - асинхронний код без
await:try { fetchData() } catchне зловить відхилений Promise. Потрібноtry { await fetchData() } catch.
finally для прибирання: зняти індикатор завантаження, розблокувати кнопку, закрити з'єднання - те, що має статися незалежно від результату.
Усі вбудовані помилки - нащадки Error. За типом зазвичай одразу зрозуміло, що пішло не так.
TypeError - операція над значенням не того типу. Найчастіша помилка у фронтенді:
const user = null;
user.name; // TypeError: Cannot read properties of null (reading 'name')
undefined(); // TypeError: undefined is not a function
const PI = 3.14; PI = 3; // TypeError: Assignment to constant variable
Зазвичай означає, що дані прийшли не такі, як очікувалося: API повернув null, елемент не знайдено на сторінці, змінну не ініціалізовано.
ReferenceError - звернення до змінної, якої не існує або яка ще не ініціалізована:
console.log(totla); // ReferenceError: totla is not defined (друкарська помилка)
console.log(x); let x = 1; // ReferenceError: Cannot access 'x' before initialization
SyntaxError - код чи дані неможливо розібрати:
JSON.parse('{ name: "Оля" }'); // SyntaxError: ключі в JSON мають бути в лапках
Синтаксична помилка в самому файлі скрипта не дає йому виконатися взагалі - жоден рядок файлу не спрацює.
RangeError - значення поза допустимими межами:
new Array(-1); // RangeError: Invalid array length
(1.5).toFixed(200); // RangeError: toFixed() digits argument must be between 0 and 100
function f() { f(); } f(); // RangeError: Maximum call stack size exceeded
URIError - некоректний URI: decodeURIComponent('%').
AggregateError - кілька помилок разом, наприклад від Promise.any, коли відхилилися всі Promise (error.errors - масив причин).
Як розрізняти в коді:
try {
data = JSON.parse(text);
} catch (error) {
if (error instanceof SyntaxError) {
showError('Сервер повернув некоректні дані');
} else {
throw error; // невідому помилку - далі, а не ковтати
}
}
Читати повідомлення уважно: Cannot read properties of undefined (reading 'map') каже не про map, а про те, що об'єкт перед .map - undefined. Шукати треба, звідки взялося це undefined.
Стек викликів (error.stack) показує, через які функції пройшов виконання до помилки, - з номерами рядків. У DevTools по ньому можна клікнути й перейти до коду.
console.log - найпростіший спосіб, але для складних помилок значно ефективніше зупинити виконання й подивитися на стан програми зсередини.
Оператор debugger у коді зупиняє виконання в цьому місці, якщо відкрито DevTools:
function calculateTotal(items) {
debugger; // виконання зупиниться тут
return items.reduce((sum, item) => sum + item.price * item.qty, 0);
}
Без відкритих DevTools оператор нічого не робить. Але в репозиторій його не комітять - лінтери на це скаржаться.
Точки зупинки в DevTools (вкладка Sources) - те саме без зміни коду: клік по номеру рядка. Види:
- умовна - зупиняється лише якщо вираз істинний (
item.price < 0). Незамінно в циклі на тисячу ітерацій; - logpoint - нічого не зупиняє, а друкує вираз у консоль. Як
console.log, але без зміни коду й перезбирання; - на подію - зупинитися на будь-якому
click,submit, таймері (панель Event Listener Breakpoints); - на зміну DOM - зупинитися, коли елемент змінюється чи видаляється (контекстне меню елемента у вкладці Elements). Відповідає на питання «хто змінює цей елемент?»;
- на запит - коли URL запиту містить рядок (XHR/fetch Breakpoints);
- на винятки - «Pause on exceptions», включно з перехопленими.
Після зупинки:
- Scope - значення всіх змінних у поточній функції й замиканнях;
- Call Stack - як ми сюди потрапили; клік на попередню функцію показує її стан;
- Watch - вирази, що перераховуються на кожному кроці;
- консоль працює в контексті зупиненої функції - можна викликати функції й змінювати змінні;
- кроки: Step over (наступний рядок), Step into (увійти у функцію), Step out (вийти з неї), Resume.
Корисні дрібниці:
$0у консолі - поточний виділений у Elements елемент;- «Blackbox script» / «Add to ignore list» - не заходити в код бібліотек (Vue, Alpine) при покроковому виконанні;
- вкладка Network - запити, відповіді, статуси; «Copy as fetch» для повторення запиту в консолі;
- у Livewire-проєктах корисно дивитися запити
livewire/updateу Network - там видно, що компонент відправив і що отримав.
Мініфікований код налагоджувати неможливо - для цього потрібні source maps, які у Vite в режимі розробки є завжди.