Представьте: вы едете на машине, наезжаете на кочку — и у машины внезапно отваливаются колёса, а двигатель глохнет. Примерно так работает JavaScript без защиты: любая мелкая ошибка может мгновенно сломать весь скрипт. Кнопка перестаёт нажиматься, интерфейс виснет, пользователь злится, бизнес теряет деньги. Поэтому для таких случаев в языке предусмотрен механизм try... catch, который позволяет перехватить аварию, «понять и простить» ошибку и дать программе работать дальше.
В этой статье разберём, как правильно оборачивать потенциально опасный код и почему так сложно поймать ошибки в асинхронных функциях.
Что такое try... catch в JavaScript и зачем он нужен
JavaScript — однопоточный язык. Это значит, что если в строке 10 произошла критическая ошибка, то строки 11, 12 и все последующие просто не выполнятся и скрипт упадёт.
try... catch — это конструкция, которая позволяет создать безопасную зону для выполнения кода:
- Вы говорите интерпретатору: «Попробуй (
try) выполнить этот кусок кода». - «А если что-то пойдёт не так, не падай, а поймай (
catch) ошибку и выполни вот этот запасной план».
Это нужно, чтобы приложение оставалось живым, даже если прилетели кривые данные с сервера или пользователь ввёл что-то не то.
Синтаксис блока try... catch: основа обработки ошибок
try... catch — это составная конструкция с обязательной связкой. Обязательный элемент здесь не только try, а пара «try + что-то».
Сам блок try запускает защищённый код, но он не может существовать в одиночку — ему обязательно нужен напарник: catch, finally или оба сразу. Можно представить это так:
try: обязательно;catch: опционально, но должен присутствовать, если нетfinally;finally: опционально, но должен быть, если нетcatch.
Важный нюанс синтаксиса: в отличие от условий if или циклов for, блоки try и catch обязаны быть в фигурных скобках {...}. Написать однострочник без скобок как if (true) doSomething() здесь не получится — JavaScript выдаст синтаксическую ошибку.
Как написать простой блок try... catch
Самый распространённый вариант использования — связка try + catch. Это базовый паттерн, который встречается в любом настоящем проекте: вы оборачиваете потенциально опасный код в try, а возможную ошибку ловите в catch:
try { ... }— здесь находится ваш основной код. Интерпретатор выполняет его построчно, до тех пор пока не встретит ошибку.catch (err) { ... }срабатывает, только если в блокеtryчто-то пошло не так и было выброшено исключение.
Минимальный рабочий пример будет таким:
try {
// Попытка вызвать несуществующую функцию
nonExistentFunction();
console.log('Эту строку никто не увидит');
} catch (error) {
// Сюда мы попадём сразу после ошибки
console.error('Поймали ошибку:', error);
}
В результате выполнение не обрывается, интерфейс остаётся живым, а ошибка контролируемо уходит в catch. А дальше мы уже можем решить, что делать с ошибкой: показать пользователю сообщение, записать лог или восстановиться.
Что такое объект ошибки (Error object)
Когда ошибка попадает в catch, JavaScript передаёт в него специальный объект с деталями сбоя. Мы можем назвать его любым именем, но чаще всего пишут error, err или e:
catch (error) {
// логика
}
Этот параметр формально необязателен — в современных стандартах можно написать просто catch { ... }. Но лучше всё же указывать его явно: так вы увидите, что именно сломалось, и сможете корректно обработать ситуацию. Обычно у него есть два главных свойства:
name— тип ошибки, например, ReferenceError;message— понятное описание (nonExistentFunction is not defined).
Допустим, мы снова вызываем несуществующую функцию и хотим посмотреть свойства ошибки:
try {
nonExistentFunction();
} catch (error) {
// Выводим отдельные свойства ошибки
console.log('Тип ошибки (name):', error.name);
console.log('Описание (message):', error.message);
console.log('Стек вызовов (stack):', error.stack);
}
Что мы увидим в консоли:
error.name: ReferenceError — это тип ошибки, в данном случае «Ошибка ссылки», то есть мы ссылаемся на то, чего нет.error.message: nonExistentFunction is not defined — понятное описание проблемы.error.stack: самое полезное для разработчика. Это список, который показывает, в какой строке и в каком файле произошёл сбой.
Именно благодаря этому объекту мы можем не просто сказать пользователю: «Упс! Что-то пошло не так», а отправить точный отчёт об ошибке себе в систему логирования.
Полезный блок со скидкой
Если вам интересно разбираться со смартфонами, компьютерами и прочими гаджетами и вы хотите научиться создавать софт под них с нуля или тестировать то, что сделали другие, — держите промокод Практикума на любой платный курс: KOD (можно просто на него нажать). Он даст скидку при покупке и позволит сэкономить на обучении.
Бесплатные курсы в Практикуме тоже есть — по всем специальностям и направлениям, начать можно в любой момент, карту привязывать не нужно, если что.
Как работает механизм try... catch: пошаговый разбор
Логика работы try... catch похожа на железнодорожную стрелку: поезд (ваш код) движется по основному пути, пока всё хорошо. Но если на пути возникает «авария», стрелка мгновенно переводится и исполнение уходит по запасному маршруту — в блок catch.
Разберём дальше на конкретных примерах.
Что происходит в блоке try
Интерпретатор заходит в этот блок и начинает выполнять инструкции одну за другой: первая строка, вторая, третья... И тут возможны два сценария.
Успех: если код выполнился до конца и нигде не рвануло, JavaScript просто пропускает блок catch и идёт дальше по скрипту.
Авария: если на какой-то строке происходит ошибка (throw exception), то:
- Выполнение кода в блоке
tryмгновенно останавливается. - Все строки, которые шли после ошибочной внутри
try, будут проигнорированы. - Управление моментально перебрасывается в начало блока
catch.
То есть catch запускается ровно в момент сбоя — ни раньше, ни позже.
Когда выполняется блок catch
Итак, блок catch запускается ровно в тот момент, когда в try произошла ошибка. И именно сюда JavaScript передаёт объект ошибки, чтобы вы могли разобраться, что именно пошло не так.
По умолчанию мы пишем:
catch (error) {
// логика
}
Но есть удобный приём: деструктуризация объекта ошибки прямо в параметре tags]catch[/tags], то есть мы можем заранее указать, какие свойства нам нужны, и получить их в виде отдельных переменных.
Смотрите: объект ошибки — это обычный объект:
{
name: "TypeError",
message: "Неверный тип данных",
stack: "...",
...другие служебные свойства
}
И поэтому при деструктуризации мы заранее говорим: «Мне нужны вот эти поля» — и получаем их как отдельные переменные:
try {
throw new TypeError("Неверный тип данных");
} catch ({ name, message }) {
// Достаём свойства сразу в переменные
console.log(name); // "TypeError"
console.log(message); // "Неверный тип данных"
}
Это особенно удобно, если вы логируете ошибки, показываете уведомления или сохраняете информацию в аналитике: так будет меньше шума и больше читабельности.
А если ошибка не важна?
Иногда нам всё равно, почему код упал, нам просто важно знать сам факт падения. Типичный пример — это проверка, является ли строка валидным JSON. Тут не нужно логировать stack trace или показывать пользователю сообщение об ошибке — мы просто хотим вернуть true/false.
С ES2019 можно вообще не писать переменную ошибки:
function isValidJSON(text) {
try {
// Если тут упадёт, значит, JSON кривой
JSON.parse(text);
return true;
} catch {
// Неважно, какая именно ошибка, просто возвращаем false
return false;
}
}
Так мы избавляемся от лишних ненужных переменных, когда контекст ошибки не играет роли.
Блок finally: для чего он нужен в конструкции try... catch
finally — это блок, который выполняется всегда: была ошибка, не было ошибки, сработал return или нет — движок всё равно дойдёт до finally перед выходом из конструкции. Основная задача finally — выполнить завершающие действия: закрыть соединение, очистить ресурсы, скрыть лоадер.
Когда выполняется блок finally
Здесь есть интересный момент, про который могут спросить на собеседованиях. Если внутри try есть инструкция return, то finally выполнится до фактического выхода из функции. Более того, если finally тоже вернет какое-то значение, оно полностью заменит результат из try.
Посмотрим на примере:
function doIt() {
try {
// Казалось бы, функция должна вернуть 1 и закончить работу
return 1;
} finally {
// Но finally перехватывает управление!
return 2;
}
}
console.log(doIt()); // Вернёт 2
Хорошая практика — это избегать операторов управления (return, throw, break) внутри finally. Они делают код запутанным и непредсказуемым. Этот блок лучше держать чистым и применять только для завершающих действий, которые должны выполниться при любом сценарии.
Практическое применение finally
Самый жизненный пример для фронтендера — индикатор загрузки.
Представьте, что вы отправляете запрос на сервер. Вам нужно включить лоадер перед запросом и выключить его, когда запрос завершится. Проблема в том, что исход запроса может быть разным: данные успешно пришли или произошла ошибка.
Если вы выключите его только в try, то при ошибке лоадер останется крутиться вечно. А если только в catch — будет крутиться даже тогда, когда всё прошло успешно.
Блок finally решает эту проблему в одну строчку:
let isLoading = true;
try {
// Пытаемся получить данные
await fetchData();
console.log('Данные загружены');
} catch (error) {
console.error('Ошибка загрузки', error);
} finally {
// Выполнится и при успехе, и при ошибке
isLoading = false;
console.log('Лоадер выключен');
}
Так код получается гораздо чище, чем если бы мы дублировали isLoading = false в двух местах.
По той же логике finally используют и в других задачах: закрыть файл (в Node.js), разорвать соединение с базой данных, очистить таймеры или отменить подписки. То есть в любых случаях, где нужно гарантированно выполнить завершающее действие, независимо от результата операции.
Типы ошибок в JavaScript: какие исключения можно отловить
Прежде чем разбирать анатомию ошибок, сразу зафиксируем два архитектурных ограничения конструкции try... catch.
Код должен быть синтаксически правильным. JavaScript-движок сначала парсит код и только потом исполняет. Если вы пропустили фигурную скобку или написали кривой синтаксис, скрипт сломается на этапе парсинга. Это называется parse-time error, и поймать её изнутри сломанного кода невозможно.
try {
function broken( { // тут какая-то синтаксическая ошибка
} catch(e) {
console.log("не сработает");
}
Такой код просто не запустится.
И да, сегодня благодаря линтерам такие ошибки делают гораздо реже: IDE сразу подсветят проблему и не дадут сохранить кривую конструкцию. Но сам принцип остаётся тем же: try... catch ловит только те ошибки, которые возникают во время выполнения, а не во время парсинга кода.
Исключение — если ошибка синтаксиса возникает в данных, а не в коде, например при парсинге JSON, и тогда её уже можно перехватить. Чуть ниже будет пример.
По умолчанию try... catch работает только в синхронном потоке. Если мы обернём в try... catch отложенный код (например, внутри setTimeout), то к моменту возникновения ошибки блок try уже давно отработает и «свернётся». Ошибка выпадет прямо в глобальную область видимости и сломает скрипт. Для асинхронщины у нас есть async/await или .catch() у промисов, но это уже другая история.
👉 О том, как правильно управлять потоком ошибок и не терять их по дороге, мы также подробно писали в статье «Как работает проброс исключений в JavaScript».
Встроенные конструкторы ошибок (Error, SyntaxError, TypeError)
В JavaScript есть стандартный набор классов ошибок. Это помогает движку, а главное, вам сразу понимать, какого рода сбой произошёл. В 99% случаев вы будете работать с одной из трёх:
Error: базовый класс.TypeError: неверный тип данных.SyntaxError: ошибка синтаксиса (в данных, а не в коде).
Разберём каждую подробно.
Error
Базовый класс, используется, когда нужно просто выбросить исключение с сообщением, не вдаваясь в детали:
throw new Error("Хьюстон, у нас проблема");
Хорош для пользовательских проверок, валидаций, бизнес-логики.
TypeError
Самый частый гость в консоли фронтендера. Возникает, когда переменная или параметр не того типа, который ожидает операция. Например, когда мы пытаемся вызвать метод у null или undefined или передаём строку вместо массива.
Например:
let user = null;
try {
// Ошибка: user is null
user.getName();
} catch (e) {
// TypeError
console.log(e.name);
}
Обычно такие ошибки говорят о проблемах с данными: не тот формат, нет валидации или API вернул неожиданную структуру.
SyntaxError
Та самая ошибка синтаксиса.
Мы уже сказали, что синтаксические ошибки в коде поймать нельзя, но вот SyntaxError можно, если он возникает не при загрузке скрипта, а при парсинге данных в рантайме.
Типичный пример — кривой JSON.
try {
JSON.parse("{ bad json o_O }");
} catch (e) {
// SyntaxError
console.log(e.name);
console.log("Пришли битые данные, сэр");
}
Это очень частый кейс в реальных приложениях: API вернул кривые данные, бэкенд отдал HTML вместо JSON, в localStorage оказались повреждённые данные и т. д.
Как создать собственную ошибку
Иногда стандартных ошибок мало. Если мы пишем сложную логику, например валидацию данных или работу с API, то важно различать «Сетевую ошибку», «Ошибку базы данных» и «Ошибку валидации».
Чтобы код понимал, какая именно проблема произошла, можно создать собственный тип ошибки. В JavaScript это делается через механизм классов: небольшое расширение базового класса Error.
Кратко пробежимся по основам классов в JS, чтобы дальше в примере было не страшно. Синтаксис class — это, по сути, удобная обёртка над прототипами:
class A extends Bозначает: «Создаём новый тип A на базе B».constructor(message)— функция, которая вызывается при создании объекта.super(message)— вызов родительского конструктора (в нашем случаеError), чтобы установить текст ошибки и служебные свойства.
И всё. Классы тут нужны только для того, чтобы создать своё имя ошибки и отличать её от других.
Теперь рассмотрим всё это на примере и создадим собственный тип ValidationError, который будет вести себя как обычная ошибка, но будет иметь своё имя:
class ValidationError extends Error {
constructor(message) {
// Вызываем родительский конструктор Error,
// чтобы корректно установить message и stack
super(message);
// Меняем имя ошибки, иначе будет просто "Error"
this.name = "ValidationError";
}
}
Теперь используем её в функции, которая читает данные пользователя:
function readUser(json) {
const user = JSON.parse(json);
if (!user.age) {
throw new ValidationError("Нет поля: возраст");
}
if (!user.name) {
throw new ValidationError("Нет поля: имя");
}
return user;
}
Дальше мы можем обработать разные ошибки по-разному:
try {
const user = readUser('{ "age": 25 }');
} catch (err) {
if (err instanceof ValidationError) {
// Ошибка валидации: показываем понятное сообщение
console.log("Некорректные данные: " + err.message);
} else if (err instanceof SyntaxError) {
// JSON оказался битым
console.log("JSON сломан: " + err.message);
} else {
// Неизвестная ошибка: пробрасываем дальше
throw err;
}
}
В итоге у нас появляется гибкая система ошибок: мы можем различать типы проблем, давать понятные сообщения пользователю и логировать нужные события.
Практические примеры использования try catch
Теперь посмотрим, где try... catch спасает прод от падения. В боевом коде мы не оборачиваем каждую строчку кода, а ставим точечную защиту там, где данные приходят извне или где мы не контролируем исполнение.
Обработка ошибок при работе с JSON (JSON.parse)
Данные, которые приходят от сервера или из localStorage, — это недоверенный источник. Даже идеально работающий бэкенд однажды может прислать HTML-страницу ошибки вместо JSON, и тогда обычный JSON.parse() сразу же сломает скрипт.
Чтобы этого не случилось, парсинг всегда должен быть защищён:
function safeParse(jsonString, fallbackValue) {
try {
return JSON.parse(jsonString);
} catch (e) {
// Логируем ошибку для себя, чтобы потом было проще
console.error("JSON parsing error:", e.message);
// Возвращаем значение по умолчанию, чтобы интерфейс не развалился
return fallbackValue;
}
}
Теперь посмотрим, как это работает в реальном сценарии:
// Бэкенд сошёл с ума
const rawData = "CRASH_ME_PLEASE";
// Подставляем запасной вариант
const config = safeParse(rawData, { theme: "dark" });
// "dark"
console.log(config.theme);
Здесь мы имитируем ситуацию из UI: вы загружаете настройки пользователя (темы, язык, фильтры, последние действия). Если данные повреждены, невалидны или их вообще нет, то интерфейс не должен падать. Вместо этого вы подставляете разумный вариант, например { theme: "dark" }.
Так приложение продолжает работать корректно: шрифты загрузятся, цвета применятся, компоненты отрендерятся. Пользователь даже не заметит, что у бэкенда случился коллапс.
Бонус для читателей
Если вам интересно погрузиться в мир ИТ и при этом немного сэкономить, держите наш промокод на курсы Практикума. Он даст вам скидку при оплате, поможет с льготной ипотекой и даст безлимит на маркетплейсах. Ладно, окей, это просто скидка, без остального, но хорошая.
Обработка ошибок при работе с API
Когда дело доходит до сетевых запросов, try... catch становится ещё полезнее. С async/await код читабелен, но сам механизм fetch может завести в ловушку: fetch кидает ошибку только тогда, когда запрос физически не состоялся — интернет пропал, домен не найден, таймаут.
А вот любые красивые ошибки 404, 500 или 403 fetch считает нормальным ответом. Поэтому в проде почти всегда добавляют ручную проверку статуса.
Посмотрим на примере:
async function getUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
// Ручная проверка статуса — это критически важно!
if (!response.ok) {
throw new Error(`HTTP Error: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
// Сюда мы попадём, если упал интернет, мы сами кинули ошибку выше или сломался парсинг JSON
if (error.message.includes("HTTP Error")) {
console.warn("Пользователь не найден или сервер лежит");
} else {
console.error("Баг парсинга", error);
}
// Пробрасываем ошибку дальше, если хотим, чтобы UI показал плашку
throw error;
}
}
Здесь мы используем довольно распространённый паттерн:
tryотвечает за успешный сценарий и распаковку данных;catchклассифицирует проблему (статус сервера? сеть? формат данных?);- в конце ошибка уходит наверх, чтобы UI мог показать нужный экран: «Пользователь не найден», «Сервер лёг», «Попробуйте снова».
Try... catch при работе с внешними библиотеками
Внешние библиотеки — это всегда риск. Сегодня всё работает хорошо, в завтра разработчики выкатили обновление, которое сломало какой-то метод. И если такая зависимость лежит прямо на горячем пути вашего кода, она реально может уронить весь интерфейс.
Чтобы этого не происходило, вызовы внешних модулей обычно изолируют в маленький «контейнер безопасности», который не даст ошибке пробиться наружу.
Допустим, мы подключили к проекту крутую математическую библиотеку:
import { complexMathLib } from "super-math-lib";
function calculatePrice(data) {
let finalPrice = 0;
try {
// Пытаемся использовать стороннюю функцию
finalPrice = complexMathLib.doMagic(data);
} catch (e) {
// Если библиотека сбоит, наше приложение не должно падать
console.warn("Библиотека отвалилась, считаем по старинке", e);
// Используем запасной, более простой алгоритм
finalPrice = data.basePrice;
}
return finalPrice;
}
Если внешний модуль не является критически важным для ядра системы, обязательно оборачивайте его вызов. Если он упал — отряхнулись и работаем дальше.
Особенности использования try... catch
Мы уже говорили, что try... catch — это механизм для синхронных ошибок. Он работает прямо в текущем стеке вызовов. Но как только код уходит «на потом» — в таймеры, обработчики событий или промисы без await, — конструкция try закрывается раньше, чем случается ошибка.
Из-за этого есть важные ограничения, которые легко пропустить в реальных проектах.
Что нельзя отловить с помощью try catch
Самая частая ловушка — это пытаться поймать ошибку, которая возникает в другом стеке вызовов. Если коротко: всё, что попадёт в Event Loop, живёт своей жизнью, а try... catch к тому моменту уже не существует.
Это напрямую продолжает идею из раздела про ошибки при парсинге и отложенный код: синтаксис ловить нельзя, и таймеры тоже нельзя. Разберёмся на примере:
try {
setTimeout(() => {
throw new Error('Ой!');
}, 1000);
console.log('Таймер запущен, try завершил работу');
} catch (e) {
console.log('Не поймал');
}
Что происходит пошагово:
- Входим в
try. - Планируем выполнение функции через 1000 мс.
- Выходим из
try— конструкция закрылась. - Проходит секунда...
- Функция внутри
setTimeoutпадает с ошибкой. - Ловить уже нечему, потому что
tryдавно закончился.
В результате ошибка летит в глобальную область видимости.
Try... catch и асинхронный код
С промисами ситуация такая же: если resolve/reject произойдёт позже, а вы не «привязали» его с await, то и try... catch не сработает.
Но с появлением async/await всё стало проще. Ключевое слово await заставляет функцию замереть, дождаться результата и — самое главное! — держит открытым try, пока промис выполняется. Из-за этого асинхронный код начинает вести себя почти как обычный линейный:
async function fetchData() {
try {
// await ставит выполнение на паузу, try тоже ждёт, пока сервер не ответит
const response = await fetch('https://broken-url');
const data = await response.json();
} catch (error) {
// Если fetch упадёт или вернёт 404/500 — мы попадём сюда
console.error('Ошибка запроса:', error.message);
}
}
Если fetch не сможет подключиться либо вернёт битые данные, то ошибка упадёт строго в catch.
Но ловушка вот где:
async function bad() {
try {
fetch('https://broken-url'); // нет await!
} catch (e) {
console.log('Не поймает!');
}
}
Промис уходит работать в фоне, вываливается из try и позже делает reject уже безо всякой страховки. Отсюда Uncaught (in promise).
👉 Подробнее о том, как всё это работает под капотом, мы писали в статье «Async/await в JavaScript».
Ищете работу в IT?
Карьерный навигатор Практикума разберёт ваше резюме, проложит маршрут к первому работодателю, подготовит к собеседованиям в 2026 году, а с января начнёт подбирать вакансии именно под вас.
Когда стоит использовать try catch
Try... catch не инструмент «на всякий случай». Используйте его только там, где реально может полыхнуть, — для обычной логики программы он не нужен и даже вреден.
Где нужно:
JSON.parse(): если придёт кривая строка с сервера, приложение упадёт. Всегда оборачивайте парсинг.async/awaitи сетевые запросы. Интернет ненадёжен: сервер может упасть, а вайфай отвалиться, поэтому оборачивайтеawait fetch(...), чтобы показать пользователю красивое сообщение «Нет связи», а не белый экран.- Ненадёжный внешний код: если вы подключаете стороннюю библиотеку или виджет, в качестве которых не уверены, — оберните их инициализацию.
Где не нужно:
- Проверка данных: не надо делать
try { user.name.toUpperCase() } catch, чтобы проверить, есть лиuser. Используйтеif (user && user.name)или опциональную цепочкуuser?.name. Это быстрее и читабельнее. - Пустой
catch:
try {
importantLogic();
} catch (e) {
// Тишина...
}
Никогда так не делайте! Код просто «проглотил» ошибку. Скрипт отработает неправильно, и вы об этом даже не узнаёте.
Короче, try... catch — это всегда страховка, а не базовая логика. Его место вокруг рискованных действий: парсинга, сетевых операций и чужого кода. Во всех остальных случаях используйте стандартные проверки.
Вам слово
Приходите к нам в соцсети поделиться своим мнением о статье и почитать, что пишут другие. А ещё там выходит дополнительный контент, которого нет на сайте — шпаргалки, опросы и разная дурка. В общем, вот тележка, вот ВК — велком!
