javascript

Цепочка ошибок в JavaScript: удобная отладка кода с помощью Error.cause

  • воскресенье, 30 ноября 2025 г. в 00:00:02
https://habr.com/ru/companies/timeweb/articles/967440/

Обработка ошибок в JavaScript всегда была немного хаотичной. Получить ошибку легко, но отследить ее первоисточник бывает очень сложно. Именно здесь и приходит на помощь свойство cause.

❯ Проблема классической обработки ошибок

В многоуровневом коде (например, сервисы, вызывающие другие сервисы, функции-обертки, «всплывающие» ошибки и т.д.) легко потерять нить того, что именно пошло не так. Обычно код при этом выглядит примерно так:

try {
  JSON.parse('{ bad json }');
} catch (err) {
  throw new Error('Something went wrong: ' + err.message);
}

Да, ошибка обернута, но при этом утрачены исходный стек вызовов и тип ошибки.

❯ Представляем Error.cause

Свойство 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: сомнительные обходные решения

До введения 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-канале