Перейти к основному содержимому

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

Разработчику Архитектору

Обработка исключений в JavaScript — это фундаментальный механизм языка программирования, предназначенный для управления ошибками и нештатными ситуациями, возникающими во время выполнения программы, который позволяет разработчику перехватывать, анализировать и корректно реагировать на ошибки, вместо того чтобы позволять им аварийно завершать работу приложения и оставлять пользователя без объяснения причин. Этот механизм построен вокруг конструкции try-catch-finally и оператора throw, которые вместе образуют структурированный подход к обработке ошибок, где потенциально опасный код помещается в блок try, ошибки перехватываются в блоке catch, а завершающие операции выполняются в блоке finally независимо от того, произошла ошибка или нет. В основе обработки исключений лежит идея разделения основного потока выполнения программы от обработки ошибочных ситуаций, что позволяет писать чистый и читаемый код, сосредоточенный на бизнес-логике, при этом все возможные ошибки обрабатываются централизованно в соответствующих блоках catch. Важно понимать, что исключения в JavaScript могут быть как синхронными, возникающими непосредственно во время выполнения кода, так и асинхронными, которые происходят в промисах, колбэках или событиях, и для обработки последних требуются отдельные механизмы, такие как метод catch у промисов или обработчики событий unhandledrejection. Правильная обработка исключений является признаком профессиональной разработки и критически важна для создания надежных, отказоустойчивых приложений, способных корректно восстанавливаться после ошибок и предоставлять пользователям понятные сообщения о проблемах вместо неинформативных крашей.

Сначала — общая теория

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

Исключение в JS — предсказуемое отклонение (нет файла, неверный аргумент), которое перехватывают и обрабатывают, чтобы продолжить сценарий.
Фатальный сбой — необработанное исключение или unhandledrejection, из‑за которых падает вкладка, скрипт или (в Node 15+) процесс.
Встроенные типы Error — в Встроенные типы ошибок и их обработка.


Синтаксис: try / catch / finally

JavaScript использует структурную обработку: блок try связывается с catch и опциональным finally.

Код ITЗагрузка примера кода…

КонструкцияНазначение
tryКод, где сбой возможен
catch (e)Обработчик; в TS предпочтительно unknown + сужение типа
finallyВыполняется всегда (очистка)
throw valueПрервать поток; для API лучше throw new Error(...)

try — это ключевое слово и структурный блок в JavaScript, который определяет участок кода, подлежащий мониторингу на предмет возникновения ошибок, и служит первым элементом в конструкции обработки исключений, внутри которого размещается тот код, выполнение которого потенциально может привести к исключительной ситуации, требующей специальной обработки. Блок try является обязательным для использования в связке с блоком catch, блоком finally или обоими одновременно, причем он создает специальный контекст выполнения, который позволяет движку JavaScript отслеживать все ошибки, возникающие внутри этого блока, и при их появлении немедленно передавать управление соответствующему блоку catch, если таковой присутствует. Важной особенностью блока try является то, что он может содержать любой исполняемый код, включая вложенные вызовы функций, асинхронные операции, обращения к внешним API и работу с DOM, причем ошибки, возникшие в любом месте внутри этого блока и не обработанные внутри вложенных конструкций, будут перехвачены внешним блоком catch. Если внутри блока try ошибка не возникает, выполнение продолжается последовательно, и блок catch пропускается, однако блок finally, если он определен, будет выполнен в любом случае, что делает try важным инструментом для гарантированного выполнения завершающих операций. Стоит отметить, что в JavaScript блок try также может использоваться с конструкцией try-with-resources, хотя в отличие от некоторых других языков, такая возможность не встроена в сам язык, но разработчики часто эмулируют подобное поведение с помощью блока finally для освобождения ресурсов, таких как закрытие соединений с базами данных или очистка таймеров. Синтаксически блок try всегда сопровождается фигурными скобками, даже если внутри него содержится всего одна строка кода, и он может быть вложенным, что позволяет организовывать иерархическую обработку ошибок на разных уровнях абстракции приложения.

catch — это ключевое слово и структурный блок в JavaScript, который следует за блоком try и предназначен для перехвата и обработки исключений, возникших внутри соответствующего блока try, причем этот блок получает в качестве параметра объект ошибки или любое другое значение, которое было передано оператору throw, и предоставляет разработчику возможность проанализировать причину ошибки и предпринять соответствующие действия. Блок catch является необязательным в конструкции try-catch-finally, но его наличие превращает потенциально опасный код в управляемую операцию, поскольку он гарантирует, что любая ошибка, возникшая в блоке try, не приведет к аварийному завершению программы, а будет обработана в соответствии с логикой, заложенной разработчиком внутри блока catch. Внутри блока catch доступен параметр, который традиционно называют error, err, e или любым другим допустимым именем, и этот параметр содержит всю доступную информацию об ошибке, включая сообщение, стек вызовов, имя ошибки и любые дополнительные свойства, которые были добавлены при создании объекта ошибки или при использовании оператора throw. Разработчик может использовать блок catch для выполнения различных действий в зависимости от типа ошибки, например, логировать ошибку в консоль или в систему мониторинга, показывать пользователю дружелюбное сообщение, пытаться повторить операцию или переключаться на альтернативный сценарий выполнения, причем внутри блока catch также может возникнуть новая ошибка, которая будет обработана внешними конструкциями обработки исключений. Важно помнить, что блок catch перехватывает только ошибки, возникшие синхронно внутри блока try, но не обрабатывает ошибки в асинхронном коде, который не находится непосредственно в блоке try, например, ошибки внутри колбэков или промисов, если они не обработаны отдельно, хотя использование асинхронных функций с await позволяет работать с ошибками через try-catch более естественным образом. В современных версиях JavaScript также существует возможность использовать условные блоки catch, где можно указать конкретный тип ошибки для перехвата, хотя эта возможность требует явной проверки типа внутри блока catch, поскольку сам язык не поддерживает множественные catch-блоки для разных типов ошибок в одной конструкции.

finally — это ключевое слово и необязательный структурный блок в JavaScript, который следует за блоками try и catch и предназначен для выполнения завершающего кода, который должен быть выполнен независимо от того, произошла ли ошибка в блоке try или нет, и был ли выполнен блок catch, что делает этот блок идеальным местом для операций очистки и освобождения ресурсов. Блок finally является уникальным элементом конструкции обработки исключений, поскольку он гарантирует выполнение своего кода в любом сценарии: если ошибка не произошла, после завершения блока try; если ошибка произошла и была перехвачена, после завершения блока catch; и даже если внутри блока catch была вызвана новая ошибка или оператор return, блок finally все равно будет выполнен до того, как управление окончательно покинет конструкцию. Это свойство делает блок finally незаменимым для таких задач, как закрытие открытых файловых дескрипторов, завершение сетевых соединений, очистка временных данных, сброс состояния таймеров или освобождение блокировок в многопоточных средах, обеспечивая тем самым предотвращение утечек ресурсов и поддержание приложения в стабильном состоянии. Важно отметить, что если внутри блока finally присутствует оператор return, он переопределяет любое возвращаемое значение из блока try или catch, что может привести к неожиданному поведению, поэтому опытные разработчики избегают использования return внутри finally, оставляя этот блок исключительно для побочных действий, не влияющих на результат работы функции. Также следует иметь в виду, что блок finally выполняется даже в том случае, если внутри блока try или catch использован оператор return, причем выполнение возврата откладывается до тех пор, пока блок finally не завершит свою работу, что демонстрирует высочайший приоритет этого блока в механизме управления потоком выполнения. В контексте асинхронного программирования блок finally также работает внутри асинхронных функций, позволяя выполнять операции очистки после завершения асинхронных операций, даже если они завершились с ошибкой, что особенно актуально при работе с базами данных или внешними сервисами, где критически важно корректно завершать соединения.

Error — это встроенный базовый класс в JavaScript, который представляет собой универсальный объект ошибки и служит основой для создания всех стандартных и пользовательских исключений, предоставляя разработчику минимально необходимый набор свойств и методов для работы с ошибками, включая имя ошибки, сообщение и стек вызовов, которые автоматически заполняются при создании экземпляра класса. Класс Error является родительским для ряда специализированных встроенных классов ошибок, таких как SyntaxError, ReferenceError, TypeError, RangeError, URIError и EvalError, каждый из которых предназначен для определенного типа ситуаций, возникающих во время выполнения JavaScript-кода, и наследует все свойства и методы базового класса, добавляя при необходимости свою специфическую семантику. При создании объекта ошибки с помощью конструктора new Error(message) можно передать строковое сообщение, которое будет храниться в свойстве message и предоставлять человекопонятное описание проблемы, а также в опциях конструктора можно передать свойство cause для указания причины возникновения текущей ошибки, создавая тем самым цепочку ошибок. Наиболее ценным свойством объекта Error является свойство stack, которое содержит строку с трассировкой стека вызовов в момент создания ошибки, показывая все функции, которые были вызваны до возникновения ошибки, с указанием имен файлов и номеров строк, что делает этот объект незаменимым инструментом для отладки и анализа причин возникновения проблем в производственных средах. Разработчики могут расширять класс Error, создавая собственные классы ошибок, которые наследуются от базового Error и добавляют свои свойства и методы, специфичные для конкретного приложения или домена, например, код ошибки, дополнительные данные о контексте, уровень критичности или информацию для восстановления после ошибки, что позволяет создавать богатую и выразительную систему обработки исключений. Важно понимать, что объекты Error в JavaScript являются сериализуемыми, но не полностью, поскольку свойство stack может не сериализоваться в некоторых окружениях, поэтому при передаче ошибок по сети или логировании их в системы мониторинга следует явно извлекать все необходимые свойства и преобразовывать их в плоские объекты для гарантии сохранения всей диагностической информации.

Раскрутка стека и поиск обработчика — в общей теории.

Интерактивное демо — сценарии на Python в компоненте ниже; в JS синтаксис try/catch, но стек тот же.

Play ITЗагрузка интерактивного демо…


throw и цепочка cause

throw — это ключевой оператор в JavaScript, который используется для генерации или выбрасывания исключений вручную, позволяя разработчику сигнализировать о возникновении ошибки в любом месте программы, когда выполнение дальнейших операций становится невозможным или нежелательным, при этом оператор throw может выбрасывать любое значение, а не только объекты ошибок, хотя на практике принято выбрасывать экземпляры класса Error или его наследников для сохранения согласованности и упрощения обработки. Когда оператор throw выполняется, текущий поток выполнения немедленно прерывается, и управление передается ближайшему блоку catch в стеке вызовов, который может перехватить выброшенное значение, причем если такой блок catch не найден ни в текущей функции, ни в вызывающих функциях, ошибка становится необработанной и приводит к аварийному завершению программы или к генерации события необработанной ошибки в асинхронном контексте. Использование оператора throw является основным способом создания пользовательских исключений, которые отражают специфические бизнес-условия или предупреждают о нарушении инвариантов, например, проверка входящих данных, отсутствие необходимых прав доступа, превышение допустимых лимитов или любые другие ситуации, когда продолжение выполнения кода может привести к повреждению данных или некорректной работе приложения. При выбрасывании ошибки разработчик может передать в оператор throw не только встроенный объект Error, но и строку с описанием проблемы, число, булево значение или даже объект с произвольной структурой, однако использование объектов Error является лучшей практикой, поскольку такие объекты автоматически содержат информацию о стеке вызовов, что значительно упрощает отладку и анализ причин возникновения ошибки. Оператор throw может использоваться не только в синхронном коде, но и внутри асинхронных функций с оператором await, где выброшенная ошибка будет отклонять возвращенный промис, а также внутри генераторов, где выброшенная ошибка может быть перехвачена с помощью метода throw генератора. Важно помнить, что выбрасывание ошибок следует применять разумно, только в действительно исключительных ситуациях, а не для управления обычным потоком выполнения, поскольку чрезмерное использование throw делает код сложным для понимания и поддержки, а также может негативно сказаться на производительности из-за затрат на построение стека вызовов.

cause — это стандартное свойство объекта ошибки в JavaScript, введенное в спецификации ECMAScript 2022, которое предназначено для сохранения ссылки на исходную ошибку, послужившую причиной возникновения текущей ошибки, что позволяет создавать цепочки ошибок и предоставлять более богатую контекстную информацию при диагностике проблем в сложных приложениях. Свойство cause является частью опций, передаваемых в конструктор ошибки при создании нового экземпляра, и хранит в себе любое значение, которое разработчик сочтет полезным для понимания первопричины ошибки, чаще всего это другой объект ошибки, но может быть и произвольный объект с дополнительными данными о контексте возникновения проблемы. Введение свойства cause решает давнюю проблему потери информации при обертывании ошибок, когда низкоуровневая ошибка перехватывается и выбрасывается новая, более высокоуровневая ошибка с обобщенным сообщением, при этом исходная ошибка терялась, что сильно затрудняло отладку, особенно в распределенных системах с множеством уровней абстракции. Используя свойство cause, разработчик может создавать иерархии ошибок, где каждая последующая ошибка добавляет свой уровень контекста, специфичного для текущего слоя приложения, при этом исходная ошибка остается доступной через цепочку cause, позволяя инженеру проследить весь путь ошибки от самого глубокого уровня до того места, где она была обработана. Свойство cause также поддерживается встроенными методами сериализации, такими как JSON.stringify, и интегрируется с инструментами логирования и мониторинга, которые могут автоматически извлекать и отображать всю цепочку причин в удобном для анализа виде. Использование свойства cause является современным стандартом обработки ошибок в JavaScript и рекомендуется для всех новых проектов, поскольку оно значительно улучшает диагностическую ценность ошибок и сокращает время, необходимое для выявления и устранения проблем в производственных средах, особенно когда ошибка проходит через несколько слоев абстракции, таких как работа с базой данных, бизнес-логика и HTTP-обработчики.

Цепочка cause — это иерархическая структура, которая образуется, когда объекты ошибок связаны между собой через свойство cause, создавая последовательную цепочку вложенных причин, где каждая следующая ошибка является оберткой для предыдущей, что позволяет разработчику проследить весь путь распространения ошибки от ее первоначального источника до конечной точки обработки на самом высоком уровне приложения. В процессе создания цепочки ошибок каждая новая ошибка добавляется в качестве причины для другой, более общей ошибки, которая затем может быть перехвачена и обработана на другом уровне, причем эта практика особенно полезна в многослойных архитектурах, таких как контроллеры, сервисы и репозитории, где каждая ошибка может быть дополнена контекстной информацией, соответствующей своему уровню абстракции. Например, ошибка низкого уровня, возникшая при выполнении запроса к базе данных, может быть обернута в ошибку уровня репозитория с добавлением информации о выполняемой операции, затем эта ошибка оборачивается в ошибку уровня сервиса с указанием бизнес-контекста, и наконец, на уровне контроллера добавляется информация о конечной точке API, в результате чего получается полноценная цепочка, которая содержит всю необходимую информацию для быстрой диагностики. Преимущество цепочки cause заключается в том, что она не теряет исходную ошибку и полностью сохраняет стек вызовов каждой ошибки в цепочке, что позволяет разработчику, анализирующему проблему в производственном окружении, увидеть всю картину целиком, начиная от самого глубокого уровня выполнения и заканчивая тем местом, где ошибка была обнаружена и обработана пользователем. Инструменты отладки и мониторинга могут автоматически обходить цепочку cause и отображать все ошибки в хронологическом порядке, представляя разработчику связный рассказ о том, как и почему произошел сбой, что значительно сокращает время на расследование инцидентов. При построении цепочек cause важно обеспечивать, чтобы каждая ошибка добавляла новую, значимую информацию, а не просто дублировала сообщение предыдущей ошибки, и чтобы цепочка не становилась бесконечной, что может привести к переполнению стека при попытке ее обхода для сериализации или логирования, поэтому практика рекомендует ограничивать глубину цепочки разумными пределами, обычно не более пяти-семи уровней.

stack — это стандартное свойство объекта Error в JavaScript, которое представляет собой строку, содержащую подробную трассировку стека вызовов, показывающую последовательность функций и методов, которые были вызваны от точки входа в программу до момента создания объекта ошибки, и является одним из самых ценных инструментов для отладки и диагностики причин возникновения исключительных ситуаций. Свойство stack автоматически заполняется движком JavaScript при создании любого объекта ошибки и включает в себя информацию о каждом вызове функции в текущем стеке, включая имена функций, имена файлов, номера строк и позиции в этих строках, что позволяет разработчику точно определить, в каком именно месте и при каких обстоятельствах произошла ошибка. Формат строки стека может незначительно отличаться в разных браузерах и версиях Node.js, но обычно она начинается с имени ошибки и ее сообщения, затем следуют строки с символами перевода строки, каждая из которых описывает один уровень стека, начиная с самого глубокого места возникновения ошибки и заканчивая точкой, где ошибка была создана. Свойство stack является незаменимым при работе с исключениями в производственных средах, поскольку оно позволяет разработчикам, имеющим доступ к логам, быстро локализовать проблему без необходимости воспроизводить ее в локальном окружении, особенно когда ошибка возникает только в специфических условиях, характерных для боевого сервера. Важно отметить, что свойство stack является нестандартным с точки зрения спецификации ECMAScript, хотя оно поддерживается всеми современными движками JavaScript и фактически стало де-факто стандартом для получения трассировки стека, и его использование в коде является безопасным и широко распространенным. При работе с асинхронным кодом стек вызовов в объекте Error может не отражать полную асинхронную цепочку, поскольку он создается в момент возникновения ошибки, а не в момент ее первоначального запуска, однако современные движки, особенно в Node.js, поддерживают асинхронную трассировку стека, которая включает информацию о контексте выполнения асинхронных операций, что значительно облегчает отладку промисов, асинхронных функций и событийных циклов.

Всегда бросайте экземпляры Error (или наследников), а не строки — так сохраняются stack и cause.

async function loadUser(id) {
try {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
return await res.json();
} catch (e) {
throw new Error(`Не удалось загрузить пользователя ${id}`, { cause: e });
}
}

В логах и DevTools видна цепочка cause — не теряйте её при обёртке (см. типичные ошибки).


Синхронный код vs Promise и async/await

КонтекстКак ловить
Синхронныйtry/catch вокруг вызова
async функцияtry/catch вокруг await
.then()второй аргумент или .catch()
Ошибка внутри колбэка thenпревращается в rejected Promise

Код ITЗагрузка примера кода…

Теория асинхронных сбоев — раздел 4.05, FAQ — Асинхронность — итоги.


unhandledrejection и глобальные обработчики

unhandledrejection — это глобальное событие, которое генерируется в JavaScript-среде, когда промис отклоняется, то есть переходит в состояние rejected, но при этом не имеет зарегистрированного обработчика для этого отклонения, такого как метод catch или второй аргумент метода then, и в результате ошибка остается необработанной на момент завершения цикла событий. Это событие является критически важным для обнаружения и обработки ошибок в асинхронном коде, поскольку в отличие от синхронных ошибок, которые перехватываются блоками try-catch, отклонения промисов без обработчиков не приводят к немедленному аварийному завершению программы, но могут сигнализировать о серьезных проблемах, которые могут привести к нестабильности приложения, утечкам памяти или некорректной обработке данных. В среде браузера событие unhandledrejection доступно через обработчик window.addEventListener, а в Node.js через событие unhandledRejection на объекте process, и в обоих случаях обработчик получает объект события, содержащий причину отклонения промиса и сам промис, который был отклонен, что позволяет разработчику предпринять соответствующие действия, такие как логирование ошибки, попытка восстановления или graceful shutdown. Обработка события unhandledrejection является обязательной практикой для любого серьезного приложения, поскольку она позволяет централизованно перехватывать все ошибки, которые не были обработаны на уровне отдельных промисов, и гарантирует, что ни одна ошибка не останется незамеченной, что особенно важно в больших проектах с множеством асинхронных операций. Современные фреймворки и инструменты мониторинга обычно автоматически подписываются на событие unhandledrejection и передают все необработанные отклонения в системы логирования и алертинга, что позволяет команде разработки оперативно реагировать на проблемы, даже если они возникают в коде, не охваченном явными обработчиками ошибок. Важно понимать, что событие unhandledrejection является последним рубежом защиты от необработанных асинхронных ошибок, и его наличие не отменяет необходимости явно обрабатывать ошибки на уровне отдельных промисов, поскольку глобальная обработка не может восстановить выполнение конкретной операции или предоставить пользователю локализованную обратную связь о проблеме, а лишь информирует о факте ошибки.

Глобальные обработчики — это централизованные механизмы перехвата и обработки ошибок на самом высоком уровне приложения, которые устанавливаются на глобальные объекты, такие как window в браузере или process в Node.js, и предназначены для перехвата ошибок, которые не были обработаны на уровне отдельных блоков try-catch или асинхронных конструкций, обеспечивая последнюю линию обороны от необработанных исключений и поддержание стабильности приложения в критических ситуациях. В веб-среде основным глобальным обработчиком является событие error на объекте window, которое перехватывает необработанные синхронные ошибки, а также ошибки в скриптах и ресурсах, в то время как событие unhandledrejection перехватывает необработанные отклонения промисов, и совместное использование этих двух обработчиков позволяет покрыть практически все типы ошибок, которые могут возникнуть в клиентском приложении. В среде Node.js основным глобальным обработчиком является событие uncaughtException на объекте process, которое перехватывает все необработанные синхронные исключения, и событие unhandledRejection для необработанных отклонений промисов, причем обработчик uncaughtException должен использоваться с особой осторожностью, поскольку после его выполнения состояние приложения может быть неопределенным и рекомендуется завершать процесс для предотвращения возможных повреждений данных. Глобальные обработчики дают возможность реализовать единую точку для логирования всех ошибок, отправки их в системы мониторинга, уведомления разработчиков, а также для выполнения необходимых операций очистки, таких как закрытие соединений с базами данных, освобождение ресурсов и сохранение состояния перед завершением процесса, что особенно важно для серверных приложений с долгим временем работы. Однако глобальные обработчики не должны рассматриваться как замена локальной обработки ошибок, а скорее как дополнительный механизм защиты, который позволяет централизованно собирать информацию об ошибках, которые по разным причинам оказались необработанными, и предотвращать полный крах приложения, давая разработчикам возможность проанализировать причины и исправить код. Важно также помнить, что в некоторых средах, особенно в Node.js, слишком активное использование глобальных обработчиков для восстановления после ошибок может привести к непредсказуемому поведению и утечкам памяти, поэтому эксперты рекомендуют использовать их исключительно для логирования и уведомления, а затем корректно завершать процесс, позволяя внешним системам, таким как менеджеры процессов или оркестраторы контейнеров, перезапускать приложение в чистом состоянии.

Необработанный rejected Promise в браузере → событие unhandledrejection. В Node — то же; на современных версиях необработанный rejection может завершить процесс.

// Браузер
window.addEventListener('unhandledrejection', (event) => {
reportToSentry(event.reason);
});

// Node
process.on('unhandledRejection', (reason) => {
console.error('Unhandled rejection:', reason);
});

Синхронные необработанные ошибки — window.onerror / process.on('uncaughtException') (последний шанс перед выходом). Подробнее — Ошибки, исключения и отказоустойчивость.


Express и границы API

В async маршрутах Express ошибки из await нужно передать в next(err) или обернуть в middleware — иначе rejection не попадёт в центральный обработчик. См. Express — middleware и ошибки.

На границе HTTP — единый формат ответа, без stack trace клиенту: обработка ошибок в API.


TypeScript

Сужение в catch: catch (e: unknown) и проверка instanceof Error. Доменные коды и ResultОбработка ошибок в TypeScript.


Связанные материалы


Что запомнить

  • Бросайте Error, сохраняйте cause при обёртке.
  • В async коде оборачивайте await в try/catch.
  • Следите за unhandledrejection в продакшене.
  • Типы встроенных ошибок — в Встроенные типы ошибок и их обработка; контракт API — отдельно от механизма throw.