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}`);
    }
}