Как работает проброс исключений в JavaScript
hard

Как работает проброс исключений в JavaScript

Пинг-понг внутри кода

Правильная обработка исключений позволяет работать с ошибками структурированным способом, чтобы отлаживать и поддерживать код было проще. Но иногда бывает нужно не обработать ошибку полностью, а передать её в более высокоуровневую часть кода. Для этого используют проброс исключений. Разбираемся, как это работает в JavaScript.

Что такое исключение

В JavaScript исключение — это какое-то событие, которое прерывает нормальное выполнение программы и сообщает о том, что произошла ошибка. Исключения могут возникать по разным причинам:

  • ошибки в синтаксисе;
  • неправильное использование функций;
  • попытки обращения к несуществующим объектам или свойствам;
  • другие ошибки, связанные с логикой программы.

Исключением может быть строка, число, логическое значение или объект. Самый простой способ обработки исключений — выбрасывать их с помощью оператора throw. При этом программа явно сигнализирует об ошибке:

function divide(a, b) {
    if (b === 0) {
        throw new Error("Деление на 0 недопустимо.");
    }
    return a / b;
}

// Выполнение этой строки вызовет выброс исключения
divide(10, 0);

Оператор throw используют в тех случаях, когда нужно указать на ошибку, например внутри функций или условий. При использовании throw выполнение кода немедленно прерывается.

Как работать с исключениями с помощью try…catch

Уровень посложнее — перехватывать и обрабатывать исключения с помощью блоков try…catch. Это обеспечивает надёжность и устойчивость программы. Конструкция try…catch оборачивает блок кода, который может выбросить исключение, и предоставляет механизм для его обработки. Если в блоке try происходит исключение, выполнение кода переходит к соответствующему блоку catch, где исключение можно обработать. Если исключение не возникает, блок catch пропускается:

try {
    let result = divide(10, 0);
    console.log(result);
} catch (error) {
    console.error("Произошла ошибка:", error.message);
}

Поскольку try…catch изолирует части кода, которые могут вызвать исключения, их можно обрабатывать, не прерывая выполнение всей программы.

На практике throw и try...catch часто применяются вместе, а в дополнение можно использовать блок finally — он выполняется независимо от того, было выброшено исключение или нет. Это полезно для очистки ресурсов или выполнения завершающих действий:

function readConfig(config) {
    if (!config) {
        throw new Error("Нет файла конфигурации");
    }
    // Дальнейшая обработка конфигурации
}

function initializeApp() {
    try {
        // Имитация отсутствия конфигурации
        let config = null; 
        readConfig(config);
    } catch (error) {
        console.error("Не удалось инициализировать приложение:", error.message);
    } finally {
        console.log("Попытка инициализации выполнена.");
    }
}

initializeApp();

Что здесь происходит:

  1. Функция readConfig выбрасывает исключение, если конфигурация отсутствует.
  2. Функция initializeApp использует try...catch для перехвата этого исключения и его обработки, а finally — для логирования ошибки и выполнения завершающих действий.

Что такое проброс исключений

Проброс исключений (или повторное выбрасывание исключений) — это процесс, при котором исключение перехватывается в блоке catch и затем снова выбрасывается для обработки на более высоком уровне в цепочке вызовов.

function processData() {
    try {
        riskyOperation();
    } catch (error) {
        console.error('Произошла ошибка:', error.message);
        // Проброс исключения для дальнейшей обработки
        throw error;
    }
}

function main() {
    try {
        processData();
    } catch (error) {
        console.error('Ошибка в функции main:', error.message);
    }
}

function riskyOperation() {
    throw new Error('Что-то пошло не так!');
}

main();

Что здесь происходит:

  1. Функция riskyOperation выбрасывает исключение с сообщением «Что-то пошло не так!».
  2. Исключение перехватывается в блоке catch функции processData. Здесь мы логируем сообщение об ошибке, а затем снова выбрасываем это исключение с помощью throw error
  3. Исключение передаётся в функцию main, где оно снова перехватывается в блоке catch, и обрабатывается (в данном случае выводится в консоль).

Зачем пробрасывать исключения

Пробрасывание позволяет выполнить локальные действия, такие как логирование ошибки или освобождение ресурсов, а затем передать исключение дальше для более общей обработки. Это помогает разделить ответственность: локальный код может позаботиться о деталях, а более высокий уровень — о глобальной стратегии обработки ошибок.

Иногда исключение нужно обработать на более высоком уровне, где есть больше информации о том, как справиться с таким исключением. Пробрасывание позволяет сохранить оригинальный контекст ошибки, предоставляя более полную картину для разработчика или системы.

Пробрасывание исключений может упростить архитектуру программы, позволяя обрабатывать ошибки в одном центральном месте, а не дублировать обработку в разных частях кода.

Когда нужно пробрасывать исключения

Проброс можно выполнить после частичной обработки исключения, если в ответ на ошибку нужно выполнить какие-то действия, например закрыть файл или соединение. При этом окончательная обработка ошибки произойдёт выше по стеку вызовов:

function readFile(filePath) {
    try {
        // Попытка прочитать файл
    } catch (error) {
        console.error(`Ошибка чтения файла ${filePath}:`, error.message);
        // Проброс исключения после логирования
        throw error;
    }
}

Если функция не знает, как правильно обработать возникшую ошибку, она может пробросить исключение, чтобы его обработал вызывающий код, который имеет необходимый контекст:

function processData(data) {
    if (!data) {
        throw new Error("Нужны данные");
    }
    // Обработка данных
}

function main() {
    try {
        processData(null);
    } catch (error) {
        // Дополнительные действия по обработке ошибки
        console.error("Ошибка при обработке данных:", error.message);
    }
}

main();

В ситуациях, когда ошибка слишком сложная или критическая, чтобы её можно было обработать локально, проброс исключения позволяет делегировать эту задачу более высокоуровневому коду:

function connectToDatabase() {
    try {
        // Попытка подключения к базе данных
    } catch (error) {
        // Проброс исключения после частичной обработки
        throw new DatabaseConnectionError("Не удалось подключиться к базе данных", error);
    }
}

function initializeApp() {
    try {
        connectToDatabase();
    } catch (error) {
        // Остановка приложения или уведомление пользователя
        console.error("Ошибка инициализации:", error.message);
    }
}

initializeApp();

Каких ошибок избегать при пробросе

Проглатывание исключений без соответствующей обработки или проброса дальше, что может затруднить отладку и понимание причин ошибок:

try {
    riskyOperation();
} catch (error) {
    // Ошибка проглочена, ничего не делается
}

✅ Вместо проглатывания исключений нужно убедиться, что ошибки обрабатываются или пробрасываются дальше:

try {
    riskyOperation();
} catch (error) {
    console.error("An error occurred:", error.message);
    // Проброс исключения для дальнейшей обработки
    throw error;
}

Проброс исключений без логирования затрудняет диагностику проблемы:

try {
    riskyOperation();
} catch (error) {
    // Исключение пробрасывается без логирования
    throw error; 
}

✅ Перед пробросом исключений их нужно логировать:

try {
    riskyOperation();
} catch (error) {
    console.error("Произошла ошибка:", error.message);
    throw error;
}

Проброс исключений из блока finally может перекрыть исключения, выброшенные в блоке try или catch.

try {
    riskyOperation();
} catch (error) {
    console.error("Произошла ошибка:", error.message);
} finally {
    // Перекроет предыдущие исключения
    throw new Error("Ошибка в финальном блоке"); 
}

✅ Чтобы исключения не перекрывались, нужно избегать их выброса в блоке finally:

try {
    riskyOperation();
} catch (error) {
    console.error("Произошла ошибка:", error.message);
    throw error;
} finally {
    console.log("Действия по очистке.");
}

Чрезмерное пробрасывание исключений по всей цепочке вызовов без необходимости — ещё одна частая ошибка:

function layer1() {
    try {
        layer2();
    } catch (error) {
        // Избыточный проброс
        throw error; 
    }
}

function layer2() {
    try {
        layer3();
    } catch (error) {
        // Избыточный проброс
        throw error; 
    }
}

function layer3() {
    throw new Error("Ошибка в layer3");
}

try {
    layer1();
} catch (error) {
    console.error("Произошла ошибка:", error.message);
}

✅ Пробрасывать исключения нужно только там, где это действительно необходимо для обработки ошибки на более высоком уровне:

function layer1() {
    // Здесь проброс исключения не нужен
    layer2(); 
}

function layer2() {
    //  Здесь проброс исключения не нужен
    layer3(); 
}

function layer3() {
    throw new Error("Ошибка в layer3");
}

try {
    layer1();
} catch (error) {
    console.error("Произошла ошибка:", error.message);
}

Усложнение логики обработки ошибок делает код трудным для чтения и поддержки:

try {
    riskyOperation();
} catch (error) {
    if (error instanceof TypeError) {
        // Проброс исключения
        throw error; 
    } else if (error instanceof RangeError) {
        // Какая-то обработка
    } else {
        console.error("Произошла непредвиденная ошибка:", error.message);
    }
}

✅ Код обработки ошибок должен быть простым и понятным:

try {
    riskyOperation();
} catch (error) {
    console.error("Произошла ошибка:", error.message);
    // Проброс исключения для дальнейшей обработки
    throw error; 
}

Альтернативы пробросу исключений

Проброс исключений — это один из способов управления ошибками в JavaScript, но есть и другие подходы, которые могут быть более уместны в зависимости от конкретного случая.

Вместо выброса исключений можно возвращать специальные значения или коды ошибок, которые сигнализируют о том, что что-то пошло не так:

function divide(a, b) {
    if (b === 0) {
        return { success: false, error: "Деление на 0" };
    }
    return { success: true, result: a / b };
}

const result = divide(10, 0);
if (!result.success) {
    console.error(result.error);
} else {
    console.log(result.result);
}

Можно использовать null или undefined — они укажут на ошибку или отсутствие результата:

function findUser(username) {
    const user = database.find(user => user.username === username);
    return user || null;
}

const user = findUser("несуществующийПользователь");
if (user === null) {
    console.error("Пользователь не найден");
} else {
    console.log("Найден пользователь:", user);
}

Ещё можно создать объект Result, который может содержать либо успешный результат, либо ошибку:

class Result {
    constructor(success, value, error) {
        this.success = success;
        this.value = value;
        this.error = error;
    }

    static ok(value) {
        return new Result(true, value, null);
    }

    static fail(error) {
        return new Result(false, null, error);
    }
}

function divide(a, b) {
    if (b === 0) {
        return Result.fail("Деление на 0");
    }
    return Result.ok(a / b);
}

const result = divide(10, 0);
if (!result.success) {
    console.error(result.error);
} else {
    console.log(result.value);
}

Если используется асинхронный код, можно передавать функции обратного вызова (callbacks), которые обрабатывают ошибки и результаты:

function fetchData(callback) {
    setTimeout(() => {
        const success = Math.random() > 0.5;
        if (success) {
            callback(null, "Данные получены");
        } else {
            callback("Ошибка получения данных", null);
        }
    }, 1000);
}

fetchData((error, data) => {
    if (error) {
        console.error(error);
    } else {
        console.log(data);
    }
});

Для обработки ошибок в асинхронном коде можно также использовать промисы:

function fetchData() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            const success = Math.random() > 0.5;
            if (success) {
                resolve("Данные получены");
            } else {
                reject("Ошибка получения данных");
            }
        }, 1000);
    });
}

fetchData()
    .then(data => console.log(data))
    .catch(error => console.error(error));

Ещё один встроенный механизм для обработки ошибок в асинхронном коде —  асинхронные функции async/await:

async function fetchData() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            const success = Math.random() > 0.5;
            if (success) {
                resolve("Данные получены");
            } else {
                reject("Ошибка получения данных");
            }
        }, 1000);
    });
}

async function main() {
    try {
        const data = await fetchData();
        console.log(data);
    } catch (error) {
        console.error(error);
    }
}

main();

Некоторые библиотеки предоставляют функциональные инструменты для обработки ошибок и управления состояниями, например folktale:

const { result: { Ok, Error } } = require('folktale');

function divide(a, b) {
    if (b === 0) {
        return Error("Деление на 0");
    }
    return Ok(a / b);
}

const result = divide(10, 0);
result.matchWith({
    Ok: ({ value }) => console.log(value),
    Error: ({ value }) => console.error(value)
});

Обложка:

Алексей Сухов

Корректор:

Ирина Михеева

Вёрстка:

Маша Климентьева

Соцсети:

Юлия Зубарева

Получите ИТ-профессию
В «Яндекс Практикуме» можно стать разработчиком, тестировщиком, аналитиком и менеджером цифровых продуктов. Первая часть обучения всегда бесплатная, чтобы попробовать и найти то, что вам по душе. Дальше — программы трудоустройства.
Получите ИТ-профессию Получите ИТ-профессию Получите ИТ-профессию Получите ИТ-профессию
Вам может быть интересно
hard