JavaScript · Синтаксис · Проміжний

Власні помилки і обробка помилок

10 завдань

Вбудовані типи помилок, власні підкласи Error та структуровані патерни обробки помилок.

Типи помилок, ієрархія, Error.stack та кастомні класи помилок

#
**Ієрархія Error** Кожна кинута помилка в JavaScript є (або повинна бути) екземпляром `Error` або одного з його підкласів: ```javascript Error // базовий клас — використовується для кастомних помилок ├── SyntaxError // невалідний синтаксис JS (зазвичай при парсингу) ├── ReferenceError // змінна не знайдена ├── TypeError // неправильний тип для операції ├── RangeError // значення поза допустимим діапазоном ├── URIError // неправильний URI └── EvalError // помилка всередині eval() (рідко зустрічається) ``` **Створення та кидання помилок** ```javascript throw new Error('Щось пішло не так'); // загальна throw new TypeError('Очікується рядок'); // конкретна throw new RangeError('Індекс поза межами'); // конкретна // Можна кидати що завгодно, але об'єкт Error дає стек викликів: throw 'oops'; // працює, але втрачає стек — уникай цього throw { code: 404 }; // та сама проблема — використовуй new Error() ``` **Властивості Error** ```javascript try { null.property; // TypeError } catch (err) { console.log(err.name); // 'TypeError' console.log(err.message); // "Cannot read properties of null (reading 'property')" console.log(err.stack); // TypeError: Cannot read properties of null (reading 'property') // at <anonymous>:2:8 // at ... } ``` `err.stack` — рядок з повідомленням про помилку та стеком викликів — незамінний для дебагінгу. Не є частиною специфікації, але всі основні двигуни його надають. **err.name vs constructor.name** ```javascript const e = new TypeError('bad'); e.name === 'TypeError' // true — мітка типу помилки e.constructor.name === 'TypeError' // теж true — але тільки якщо клас не мініфікований // Краще перевіряти err.name або instanceof у catch-блоках: if (err instanceof TypeError) { ... } // найчистіше if (err.name === 'TypeError') { ... } // теж добре // Не покладайся на err.constructor.name у production (мініфікація може змінити) ``` **Кастомні класи помилок** ```javascript class ValidationError extends Error { constructor(message, field) { super(message); this.name = 'ValidationError'; // перевизначити — важливо! this.field = field; } } throw new ValidationError('Обов\'язкове поле відсутнє', 'email'); // У catch: if (err instanceof ValidationError) { console.log(`Поле '${err.field}': ${err.message}`); } ``` Завжди встановлюй `this.name` у кастомних підкласах — інакше `err.name` успадкує 'Error' від базового класу, що ламає перевірку типу за рядком.

try/catch/finally, перекидання та необроблені відхилення промісів

#
**try / catch / finally — основи** ```javascript try { const data = JSON.parse(input); // може кинути SyntaxError processData(data); // може кинути інші помилки } catch (err) { console.error('Помилка:', err.message); } finally { cleanup(); // виконується незалежно від того, чи була помилка } ``` `finally` завжди виконується — навіть якщо блок `try` містить `return`. Використовуй для очищення: закриття файлів, зняття блокувань, скидання стану завантаження. **Перехоплення конкретних типів помилок** ```javascript try { riskyOperation(); } catch (err) { if (err instanceof TypeError) { console.error('Проблема з типом:', err.message); } else if (err instanceof ValidationError) { showFieldError(err.field, err.message); } else { throw err; // перекидай помилки, які не можеш обробити } } ``` **Перекидання — не заглушуй помилки, які не можеш обробити** Поширена помилка — мовчки перехоплювати всі помилки: ```javascript // ПОГАНО — приховує баги: try { riskyOp(); } catch (err) { /* ігнорується */ } // КРАЩЕ — обробляй що можеш, перекидай решту: try { riskyOp(); } catch (err) { if (err instanceof NetworkError) { return fallbackData(); } throw err; // щось несподіване — дозволь поширитись } ``` **Відхилення Promise vs синхронний throw** Помилки всередині `async`-функцій стають відхиленими промісами: ```javascript async function loadUser(id) { const user = await fetchUser(id); // кидає при помилці → відхиляє проміс return user; } // Треба перехоплювати на місці виклику: loadUser(42) .then(user => render(user)) .catch(err => showError(err.message)); // Або з await + try/catch: try { const user = await loadUser(42); } catch (err) { showError(err.message); } ``` **Необроблені відхилення промісів** ```javascript // Це відхилення залишається необробленим — часто призводить до краша в Node.js: fetch('/api/data'); // забули .catch() // Виявляти глобально: window.addEventListener('unhandledrejection', (event) => { console.error('Необроблене відхилення:', event.reason); event.preventDefault(); }); // У Node.js: process.on('unhandledRejection', (reason, promise) => { console.error('Необроблене відхилення у:', promise, 'причина:', reason); }); ```

Стратегії обробки помилок: throw vs Result-патерн, Error.cause

#
**Стратегії обробки помилок** Існують три патерни для повідомлення про збої. Кожен має своє місце: ``` 1. Throw — для несподіваних/невідновлюваних помилок (програмні баги) 2. Повернути null/undefined — простий сентинел, але втрачає деталі помилки 3. Result-об'єкт {ok, error} — явний, типізований, змушує викликача перевіряти ``` **Патерн Result** Замість throw для очікуваних збоїв повертай дискримінований union: ```javascript function parseUserInput(str) { if (!str.trim()) { return { ok: false, error: 'Введення порожнє' }; } const num = Number(str); if (isNaN(num)) { return { ok: false, error: `'${str}' не є числом` }; } return { ok: true, value: num }; } const result = parseUserInput(userStr); if (!result.ok) { showError(result.error); } else { process(result.value); } ``` Використовуй патерн Result для **очікуваних збоїв** (валідація форм, парсинг, API-відповіді, яких може не бути). Використовуй `throw` для **несподіваного стану** (помилки програміста, порушені інваріанти). **Патерн Error Boundary — глобальний async-обробник** ```javascript async function main() { try { const config = await loadConfig(); await startServer(config); } catch (err) { console.error('Фатальна помилка:', err); process.exit(1); } } main(); ``` **Обгортання помилок для збереження контексту** ES2022 додав `Error.cause` для ланцюгування помилок: ```javascript async function loadUserProfile(id) { try { return await db.users.findById(id); } catch (err) { throw new Error(`Не вдалося завантажити користувача ${id}`, { cause: err }); } } // У catch: console.error(err.message); // 'Не вдалося завантажити користувача 42' console.error(err.cause.message); // оригінальне повідомлення помилки БД ``` **Короткий посібник з вибору підходу** ``` Сценарій Рекомендований підхід ────────────────────────────────── ────────────────────────────────── Валідація введення користувача Патерн Result {ok, error} Збої мережі/БД throw (async-межа перехопить) Помилка програміста (assert-подібне) throw Error з описом Необов'язкові дані, яких може не бути return null (простий випадок) Кілька можливих режимів збою Result з типізованою помилкою ```
01

#

Напишіть функцію, яка безпечно ділить два числа. Якщо дільник дорівнює 0, кидайте `RangeError` з повідомленням 'Division by zero'.

function safeDivide(a, b) {
    // ваш код тут
}

console.log(safeDivide(10, 2));  // 5
try {
    safeDivide(10, 0);
} catch (e) {
    console.log(e instanceof RangeError);  // true
    console.log(e.message);                // 'Division by zero'
}
Рішення
function safeDivide(a, b) {
    if (b === 0) throw new RangeError('Division by zero');
    return a / b;
}
02

#

Створіть власний клас помилки `ValidationError`, що розширює `Error`. Він повинен приймати назву поля та повідомлення, зберігаючи їх як властивості.

class ValidationError extends Error {
    constructor(field, message) {
        // ваш код тут
    }
}

const e = new ValidationError('email', 'Invalid format');
console.log(e instanceof ValidationError);  // true
console.log(e instanceof Error);            // true
console.log(e.field);                       // 'email'
console.log(e.message);                     // 'Invalid format'
console.log(e.name);                        // 'ValidationError'
Рішення
class ValidationError extends Error {
    constructor(field, message) {
        super(message);
        this.name = 'ValidationError';
        this.field = field;
    }
}
03

#

Напишіть блок `try/catch/finally`, що розбирає рядок JSON. Якщо розбір не вдається, поверніть значення за замовчуванням `{}`. Блок `finally` повинен логувати 'parse attempted'.

function safeParse(str) {
    // ваш код тут
}

console.log(safeParse('{"a":1}'));   // { a: 1 }
console.log(safeParse('invalid'));    // {}
// 'parse attempted' повинно логуватися в обох випадках
Рішення
function safeParse(str) {
    try {
        return JSON.parse(str);
    } catch (e) {
        return {};
    } finally {
        console.log('parse attempted');
    }
}
04

#

Напишіть функцію, що приймає масив та індекс. Якщо індекс виходить за межі, кидайте `RangeError`. Якщо аргумент не є масивом, кидайте `TypeError`.

function getElement(arr, index) {
    // ваш код тут
}

console.log(getElement([1, 2, 3], 1));  // 2
try { getElement('not array', 0); } catch(e) { console.log(e.constructor.name); } // TypeError
try { getElement([1,2], 5); } catch(e) { console.log(e.constructor.name); }       // RangeError
Рішення
function getElement(arr, index) {
    if (!Array.isArray(arr)) throw new TypeError('Expected an array');
    if (index < 0 || index >= arr.length) throw new RangeError(`Index ${index} out of bounds`);
    return arr[index];
}
05

#

Напишіть функцію, що перехоплює специфічний тип помилки та повторно кидає будь-які інші помилки, які вона не знає, як обробляти.

class NetworkError extends Error {
    constructor(msg) { super(msg); this.name = 'NetworkError'; }
}

function handleRequest(fn) {
    // Викликайте fn(). Якщо кидає NetworkError, поверніть 'network error handled'.
    // Якщо кидає щось інше, повторно кидайте.
}

console.log(handleRequest(() => { throw new NetworkError('timeout'); }));
// 'network error handled'
try {
    handleRequest(() => { throw new TypeError('bad type'); });
} catch (e) {
    console.log(e instanceof TypeError);  // true
}
Рішення
class NetworkError extends Error {
    constructor(msg) { super(msg); this.name = 'NetworkError'; }
}

function handleRequest(fn) {
    try {
        return fn();
    } catch (e) {
        if (e instanceof NetworkError) return 'network error handled';
        throw e;
    }
}
06

#

Обробіть асинхронні помилки: напишіть асинхронну функцію, що завантажує URL (симульований). Якщо завантаження не вдається, логуйте повідомлення про помилку і поверніть null.

async function simulatedFetch(url) {
    if (!url.startsWith('https')) throw new Error(`Invalid URL: ${url}`);
    return { status: 200, data: 'ok' };
}

async function safeFetch(url) {
    // ваш код тут
}

safeFetch('https://example.com').then(console.log);  // { status: 200, data: 'ok' }
safeFetch('http://bad.com').then(console.log);       // null (та логує помилку)
Рішення
async function safeFetch(url) {
    try {
        return await simulatedFetch(url);
    } catch (e) {
        console.error(e.message);
        return null;
    }
}
07

#

Напишіть функцію, що обгортає іншу функцію і перехоплює будь-який TypeError, повертаючи натомість значення за замовчуванням. Інші типи помилок повинні поширюватися нормально.

function withTypeErrorFallback(fn, defaultValue) {
    // ваш код тут
}

const safe = withTypeErrorFallback(() => null.length, 0);
console.log(safe);  // 0

try {
    withTypeErrorFallback(() => { throw new RangeError('oops'); }, 0);
} catch (e) {
    console.log(e instanceof RangeError);  // true
}
Рішення
function withTypeErrorFallback(fn, defaultValue) {
    try {
        return fn();
    } catch (e) {
        if (e instanceof TypeError) return defaultValue;
        throw e;
    }
}
08

#

Створіть клас `ErrorRegistry`, що записує всі помилки, кинуті під час пакетної обробки. Він повинен їх збирати, а не зупинятися на першій.

class ErrorRegistry {
    constructor() {
        this.errors = [];
    }

    run(fn) {
        // Викликайте fn(). Якщо кидає, записуйте помилку. Ніколи не перекидайте.
    }

    hasErrors() { return this.errors.length > 0; }
    getErrors() { return this.errors; }
}

const reg = new ErrorRegistry();
reg.run(() => { /* ok */ });
reg.run(() => { throw new Error('first'); });
reg.run(() => { throw new TypeError('second'); });
console.log(reg.hasErrors());           // true
console.log(reg.getErrors().length);    // 2
Рішення
class ErrorRegistry {
    constructor() {
        this.errors = [];
    }

    run(fn) {
        try {
            fn();
        } catch (e) {
            this.errors.push(e);
        }
    }

    hasErrors() { return this.errors.length > 0; }
    getErrors() { return this.errors; }
}
09

#

Реалізуйте ланцюжок помилок: створіть функцію, що обгортає низькорівневу помилку у високорівневу, зберігаючи оригінальну помилку як `cause`.

function readConfig(filename) {
    throw new Error(`File not found: ${filename}`);
}

function loadApp(configPath) {
    // Спробуйте readConfig(configPath). Якщо не вдається, кидайте новий Error
    // 'Failed to load application' з оригіналом як 'cause'
}

try {
    loadApp('config.json');
} catch (e) {
    console.log(e.message);        // 'Failed to load application'
    console.log(e.cause.message);  // 'File not found: config.json'
}
Рішення
function readConfig(filename) {
    throw new Error(`File not found: ${filename}`);
}

function loadApp(configPath) {
    try {
        return readConfig(configPath);
    } catch (e) {
        throw new Error('Failed to load application', { cause: e });
    }
}
10

#

Напишіть функцію `assertType`, що кидає `TypeError` з описовим повідомленням, якщо значення не є очікуваного типу (перевіряється через `typeof`).

function assertType(value, expectedType) {
    // ваш код тут
}

assertType(42, 'number');       // ok, не кидає
assertType('hello', 'string');  // ok
try {
    assertType(42, 'string');
} catch (e) {
    console.log(e instanceof TypeError);  // true
    console.log(e.message);  // наприклад 'Expected string, got number'
}
Рішення
function assertType(value, expectedType) {
    const actual = typeof value;
    if (actual !== expectedType) {
        throw new TypeError(`Expected ${expectedType}, got ${actual}`);
    }
}