Правильная обработка исключений позволяет работать с ошибками структурированным способом, чтобы отлаживать и поддерживать код было проще. Но иногда бывает нужно не обработать ошибку полностью, а передать её в более высокоуровневую часть кода. Для этого используют проброс исключений. Разбираемся, как это работает в 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();
Что здесь происходит:
- Функция 
readConfigвыбрасывает исключение, если конфигурация отсутствует. - Функция 
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();
Что здесь происходит:
- Функция 
riskyOperationвыбрасывает исключение с сообщением «Что-то пошло не так!». - Исключение перехватывается в блоке 
catchфункцииprocessData. Здесь мы логируем сообщение об ошибке, а затем снова выбрасываем это исключение с помощьюthrow error - Исключение передаётся в функцию 
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)
});
						
