Урок 20 из 49 · Месяц 3. JavaScript, часть 2
Асинхронность, часть 2: try, catch, finally
Содержание урока
- Что происходит при ошибке
- Конструкция try...catch
- Объект ошибки
- Практический пример: безопасный JSON.parse
- Блок finally
- Создаём свои ошибки: throw
- Свой класс ошибки
- Ошибки в асинхронном коде
- Ловушка с setTimeout
- Ошибки в промисах
- Забегая вперёд: try...catch с await
- Как правильно обрабатывать ошибки
- Практика
- Итоги
Ошибки в программе неизбежны: сервер не ответил, пользователь ввёл буквы вместо числа, в JSON пришёл мусор. Плохо, когда из-за одной ошибки «ломается» весь сайт. В этом уроке научимся перехватывать ошибки с помощью конструкции try...catch...finally, создавать свои ошибки через throw и правильно обрабатывать ошибки в асинхронном коде.
Что происходит при ошибке
Если в коде возникает ошибка и её никто не обработал, JavaScript останавливает выполнение текущего скрипта:
console.log("Начало");
const user = null;
console.log(user.name); // TypeError: Cannot read properties of null
console.log("Конец"); // эта строка уже не выполнится
В консоли появится красное сообщение, а всё, что ниже ошибки, не выполнится. На реальном сайте это может означать неработающую кнопку или пустую страницу.
Конструкция try...catch
try...catch — это «страховочная сетка». Мы говорим JavaScript: «Попробуй выполнить этот код. Если что-то сломается — не падай, а передай ошибку мне».
try {
// код, в котором может быть ошибка
} catch (error) {
// что делать, если ошибка случилась
}
Перепишем пример:
console.log("Начало");
try {
const user = null;
console.log(user.name);
console.log("Эта строка не выполнится");
} catch (error) {
console.log("Поймали ошибку:", error.message);
}
console.log("Конец"); // выполнится!
Результат:
Начало
Поймали ошибку: Cannot read properties of null (reading 'name')
Конец
Как это работает:
- Выполняется код внутри
try. - Если ошибок нет — блок
catchпропускается. - Если ошибка возникла — выполнение
tryсразу прерывается, и управление переходит вcatch. - После
catchпрограмма продолжает работу как обычно.
Аналогия — акробат с сеткой. Если трюк получился — сетка не нужна. Если сорвался — сетка ловит, и представление продолжается.
Объект ошибки
В catch попадает объект ошибки с полезными свойствами:
| Свойство | Что содержит | Пример |
|---|---|---|
name |
тип ошибки | "TypeError" |
message |
понятное описание | "Cannot read properties of null" |
stack |
где именно произошла ошибка (цепочка вызовов) | для отладки |
try {
undefinedFunction();
} catch (error) {
console.log(error.name); // "ReferenceError"
console.log(error.message); // "undefinedFunction is not defined"
}
Встроенные типы ошибок, которые вы будете встречать чаще всего:
ReferenceError— обращение к несуществующей переменной;TypeError— действие не подходит для типа значения (например, вызвать не-функцию или прочитать свойство уnull);SyntaxError— неправильный синтаксис, например приJSON.parseнекорректной строки;RangeError— значение вне допустимого диапазона (например, переполнение стека при рекурсии).
Практический пример: безопасный JSON.parse
Вспомните урок про JSON и localStorage. Если в localStorage лежит испорченная строка, JSON.parse выбросит ошибку — и весь скрипт остановится. Защитимся:
function loadCart() {
const raw = localStorage.getItem("cart");
try {
return JSON.parse(raw) ?? [];
} catch (error) {
console.warn("Корзина повреждена, начинаем с пустой:", error.message);
localStorage.removeItem("cart");
return [];
}
}
localStorage.setItem("cart", "{это не json");
console.log(loadCart()); // [] — и сайт продолжает работать
Разбор:
- Если в хранилище ничего нет,
getItemвернётnull,JSON.parse(null)вернётnull, и оператор??подставит пустой массив. - Если строка испорчена,
JSON.parseвыброситSyntaxError, мы попадём вcatch, удалим мусор и вернём пустую корзину.
Блок finally
finally выполняется всегда: была ошибка или нет, и даже если внутри try или catch сработал return.
function divide(a, b) {
try {
if (b === 0) throw new Error("Делить на ноль нельзя");
return a / b;
} catch (error) {
console.log("Ошибка:", error.message);
return null;
} finally {
console.log("Операция деления завершена");
}
}
console.log(divide(10, 2)); // "Операция деления завершена", потом 5
console.log(divide(10, 0)); // "Ошибка: ...", "Операция деления завершена", потом null
Зачем нужен finally? Для «уборки», которая должна произойти в любом случае:
- спрятать индикатор загрузки (спиннер);
- снова разблокировать кнопку отправки формы;
- закрыть соединение или сбросить флаг.
const button = document.querySelector("#save");
function save() {
button.disabled = true;
button.textContent = "Сохраняем...";
try {
// какая-то работа, которая может упасть
saveToStorage();
} catch (error) {
alert("Не удалось сохранить");
} finally {
button.disabled = false; // кнопка вернётся в любом случае
button.textContent = "Сохранить";
}
}
Можно писать try...finally и без catch — тогда ошибка не перехватится, но «уборка» всё равно выполнится.
Создаём свои ошибки: throw
Оператор throw («бросить») позволяет самим создать ошибку, когда что-то пошло не по плану.
function setAge(age) {
if (typeof age !== "number" || Number.isNaN(age)) {
throw new TypeError("Возраст должен быть числом");
}
if (age < 0 || age > 120) {
throw new RangeError("Возраст должен быть от 0 до 120");
}
return age;
}
try {
setAge(150);
} catch (error) {
console.log(`${error.name}: ${error.message}`);
// "RangeError: Возраст должен быть от 0 до 120"
}
throwнемедленно прерывает функцию — какreturn, но с ошибкой.- Бросать можно что угодно, но правильно бросать объект
Error(или его разновидности): у него естьmessageиstack.
Свой класс ошибки
Когда ошибок много, удобно отличать их по типу. Для этого создают собственный класс, наследуя Error:
class ValidationError extends Error {
constructor(field, message) {
super(message); // передаём сообщение в родительский Error
this.name = "ValidationError";
this.field = field; // дополнительная информация — какое поле
}
}
function validateUser(user) {
if (!user.email.includes("@")) {
throw new ValidationError("email", "Неверный email");
}
}
try {
validateUser({ email: "anna.mail.com" });
} catch (error) {
if (error instanceof ValidationError) {
console.log(`Поле ${error.field}: ${error.message}`);
} else {
throw error; // неизвестную ошибку не «глотаем», а пробрасываем дальше
}
}
extends Error— наш класс умеет всё, что умеет обычная ошибка.instanceofпроверяет, к какому классу относится ошибка.- Проброс (
throw errorвcatch) — если ошибка не та, которую мы умеем обрабатывать, отдаём её выше. Так мы не скрываем настоящие баги.
Ошибки в асинхронном коде
Ловушка с setTimeout
try...catch работает синхронно. Он не поймает ошибку, которая случится позже, внутри колбэка:
try {
setTimeout(() => {
throw new Error("Ошибка в таймере");
}, 1000);
} catch (error) {
console.log("Поймали?"); // НЕ выполнится
}
Почему так? Вспомните урок про Event Loop: к моменту, когда сработает таймер, блок try давно закончился — стек вызовов пуст. Ловить ошибку надо внутри колбэка:
setTimeout(() => {
try {
throw new Error("Ошибка в таймере");
} catch (error) {
console.log("Поймали:", error.message);
}
}, 1000);
Ошибки в промисах
У промисов есть свои аналоги: .catch вместо catch, .finally вместо finally. Если внутри .then выбросить ошибку через throw, промис станет rejected и ошибка уйдёт в ближайший .catch:
function getUser(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
id > 0 ? resolve({ id, name: "Анна" }) : reject(new Error("Пользователь не найден"));
}, 500);
});
}
showLoader();
getUser(1)
.then((user) => {
if (!user.email) throw new Error("У пользователя нет email");
return user;
})
.then((user) => console.log(user))
.catch((error) => console.log("Ошибка:", error.message)) // "У пользователя нет email"
.finally(() => hideLoader());
| Синхронный код | Промисы |
|---|---|
try { ... } |
.then(...) |
catch (error) { ... } |
.catch((error) => ...) |
finally { ... } |
.finally(() => ...) |
throw new Error() |
throw внутри .then или reject(new Error()) |
Забегая вперёд: try...catch с await
Есть способ писать асинхронный код так, чтобы обычный try...catch работал и для промисов. Для этого функцию помечают словом async, а перед промисом пишут await — «дождись результата». Подробно разберём это в уроке про fetch и async/await, а пока просто посмотрите, как это выглядит:
async function showUser() {
showLoader();
try {
const user = await getUser(-1); // ждём промис
console.log(user.name);
} catch (error) {
console.log("Ошибка:", error.message); // "Пользователь не найден"
} finally {
hideLoader();
}
}
showUser();
Здесь await превращает отклонённый промис в обычную ошибку, которую ловит catch. Код читается как синхронный — сверху вниз.
Как правильно обрабатывать ошибки
- Не оборачивайте в
tryвсё подряд. Ставьте его там, где ошибка реально возможна: разбор JSON, запросы к серверу, работа с данными от пользователя. - Не «глотайте» ошибки. Пустой
catch {}скрывает баги. Хотя бы выведитеconsole.error(error). - Показывайте пользователю понятное сообщение, а технические детали — в консоль.
- Используйте
finallyдля действий, которые нужны в любом исходе.
try {
riskyOperation();
} catch (error) {
console.error(error); // для разработчика
showMessage("Что-то пошло не так. Попробуйте ещё раз."); // для пользователя
}
Справка: MDN: try...catch.
Практика
- Безопасное число. Напишите функцию
parseNumber(str), которая возвращает число из строки, а если строка не число — выбрасываетError("Это не число"). Вызовите её с"42"и"abc"внутриtry...catchи выведите результат или сообщение об ошибке. - finally. Добавьте к первому заданию блок
finally, который выводит «Проверка завершена». Убедитесь, что он срабатывает в обоих случаях. - Загрузка настроек. Напишите
loadSettings(), которая читает объектsettingsизlocalStorage. Если там испорченный JSON — вернуть объект по умолчанию{ theme: "light", lang: "ru" }. Проверьте, записав в хранилище строку"{oops". - Свой класс. Создайте класс
NotFoundErrorи функциюfindProduct(id), которая ищет товар в массиве и выбрасываетNotFoundError, если не нашла. Вcatchвыведите разное сообщение дляNotFoundErrorи для остальных ошибок. - Промис с обработкой. Используя
getUserиз урока, напишите цепочку, которая показывает текст «Загрузка...» в элементе на странице, затем имя пользователя или текст ошибки, а в.finallyвыводит в консоль «Готово».
Итоги
- Необработанная ошибка останавливает выполнение скрипта.
try— пробуем,catch— обрабатываем ошибку,finally— выполняем в любом случае.- У объекта ошибки есть
name,messageиstack. throw new Error("...")создаёт свою ошибку; можно наследоватьErrorдля своих типов.- Обычный
try...catchне ловит ошибки из колбэковsetTimeout— их ловят внутри колбэка. - В промисах роль
catchиfinallyиграют.catch()и.finally(), а сawaitработает обычныйtry...catch.