Цепочка ошибок в JavaScript: удобная отладка кода с помощью Error.cause
- воскресенье, 30 ноября 2025 г. в 00:00:02
Обработка ошибок в JavaScript всегда была немного хаотичной. Получить ошибку легко, но отследить ее первоисточник бывает очень сложно. Именно здесь и приходит на помощь свойство cause.
В многоуровневом коде (например, сервисы, вызывающие другие сервисы, функции-обертки, «всплывающие» ошибки и т.д.) легко потерять нить того, что именно пошло не так. Обычно код при этом выглядит примерно так:
try {
JSON.parse('{ bad json }');
} catch (err) {
throw new Error('Something went wrong: ' + err.message);
}
Да, ошибка обернута, но при этом утрачены исходный стек вызовов и тип ошибки.
Свойство cause позволяет аккуратно сохранить исходную ошибку:
try {
try {
JSON.parse('{ bad json }');
} catch (err) {
throw new Error('Something went wrong', { cause: err });
}
} catch (err) {
console.error(err.stack);
console.error('Caused by:', err.cause.stack);
}
Вот что происходит при использовании Error.cause (обратите внимание, что теперь доступны оба стека вызовов):
Error: Something went wrong
at ...
Caused by: SyntaxError: Unexpected token b in JSON at position 2
at JSON.parse (<anonymous>)
at ...
Теперь исходная ошибка сохраняется, а на верхнем уровне отображается понятное сообщение.
function fetchUserData() {
try {
JSON.parse('{ broken: true }'); // ← Здесь возникнет ошибка
} catch (parseError) {
throw new Error('Failed to fetch user data', { cause: parseError });
}
}
try {
fetchUserData();
} catch (err) {
console.error(err.message); // "Failed to fetch user data"
console.error(err.cause); // [SyntaxError: Unexpected token b in JSON]
console.error(err.cause instanceof SyntaxError); // true
}
Получается довольно удобно.
Свойство cause по спецификации не является перечисляемым (enumerable) при передаче через конструктор Error, поэтому оно не попадает в логи и циклы for...in, если не обращаться к нему явно. То же самое касается свойств message и stack.
⚠️Примечание: JS не объединяет стеки вызовов автоматически. Стек нового объекта ошибки отображается отдельно. Чтобы получить полный стек вызовов, необходимо вручную обратиться к
err.cause.stack.
До введения cause в ES2022 разработчики использовали разные «костыли»: конкатенацию строк, собственные свойства вроде .originalError или полное оборачивание ошибки. Эти методы часто приводили к потере важных данных, таких как исходный стек вызовов или тип ошибки.
Свойство cause решает эту проблему стандартным чистым способом.
cause можно использовать и в собственных классах ошибок:
class DatabaseError extends Error {
constructor(message, { cause } = {}) {
super(message, { cause });
this.name = 'DatabaseError';
}
}
Если используется среда выполнения ES2022 и новее, этого достаточно — super(message, { cause }) обработает все автоматически.
Для TypeScript важно, чтобы в tsconfig.json были правильно настроены следующие параметры:
{
"compilerOptions": {
"target": "es2022",
"lib": ["es2022"]
}
}
Иначе при передаче { cause } в конструктор Error возникнет ошибка типа.
Цепочка ошибок полезна не только во время выполнения, но и при тестировании.
Предположим, сервис выбрасывает UserCreationError, вызванный ValidationError. Вместо проверки только верхнеуровневой ошибки можно определить следующее утверждение:
expect(err.cause).toBeInstanceOf(ValidationError);
Тесты становятся более надежными и понятными.
По умолчанию console.error(err) выводит только верхнеуровневую ошибку. Цепочка cause не отображается автоматически, поэтому ее нужно логировать вручную:
console.error(err);
console.error('Caused by:', err.cause);
Не нужно увлекаться этим слишком сильно. Если логировать каждую мелкую ошибку, отладка может стать еще более запутанной. Используйте это там, где контекст действительно важен.
Вот небольшой вспомогательный код, который безопасно обходит цепочку ошибок:
function logErrorChain(err, level = 0) {
if (!err) return;
console.error(' '.repeat(level * 2) + `${err.name}: ${err.message}`);
if (err.cause instanceof Error) {
logErrorChain(err.cause, level + 1);
} else if (err.cause) {
console.error(' '.repeat((level + 1) * 2) + String(err.cause));
}
}
А вот код для вывода полного стека вызовов:
function logFullErrorChain(err) {
let current = err;
while (current) {
console.error(current.stack);
current = current.cause instanceof Error ? current.cause : null;
}
}
Отлично подходит для сложных систем, где на разных уровнях может возникать множество ошибок.
Представим такой сценарий:
Обращение к базе данных завершилось ошибкой ConnectionTimeoutError.
Она была перехвачена и повторно выброшена как DatabaseError.
Эта ошибка снова была перехвачена и обернута в ServiceUnavailableError.
class ConnectionTimeoutError extends Error {}
class DatabaseError extends Error {}
class ServiceUnavailableError extends Error {}
try {
try {
try {
throw new ConnectionTimeoutError('DB connection timed out');
} catch (networkErr) {
throw new DatabaseError('Failed to connect to database', { cause: networkErr });
}
} catch (dbErr) {
throw new ServiceUnavailableError('Unable to save user data', { cause: dbErr });
}
} catch (finalErr) {
logErrorChain(finalErr);
}
Вывод в консоли:
ServiceUnavailableError: Unable to save user data
DatabaseError: Failed to connect to database
ConnectionTimeoutError: DB connection timed out
Параметр .cause поддерживается во всех современных средах:
✅ Chrome 93+, Firefox 91+, Safari 15+, Edge 93+
✅ Node.js 16.9+
✅ Bun и Deno (актуальные версии)
⚠️ Примечание: DevTools могут не показывать
causeавтоматически. Нужно выводить его явно черезconsole.error('Caused by:', err.cause). Если код транспилируется с помощью Babel или TS, эта возможность не полифилится.
📌 Более современные подходы
Если цель — писать аккуратный асинхронный код,
Array.fromAsync()станет отличным помощником.
✅ Используем new Error(message, { cause }), чтобы сохранять контекст
✅ Работает со встроенными и кастомными классами ошибок
✅ Поддерживается во всех современных средах (браузеры, Node, Deno, Bun)
✅ Улучшает логи, отладку и тестирование
✅ TS: указываем "target": "es2022" и "lib": ["es2022"]
⚠️ Не забываем явно логировать err.cause или обходить цепочку ошибок вручную
Чистые стеки вызовов. Полный контекст. Лучшая отладка.
Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud - в нашем Telegram-канале ↩