Правильная обработка исключений позволяет работать с ошибками структурированным способом, чтобы отлаживать и поддерживать код было проще. Но иногда бывает нужно не обработать ошибку полностью, а передать её в более высокоуровневую часть кода. Для этого используют проброс исключений. Разбираемся, как это работает в 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)
});