1. Почему обработка ошибок в асинхронном коде — это важно
В синхронном коде всё просто: если функция выбрасывает ошибку (throw), выполнение кода прерывается и ошибка "всплывает" вверх по стеку вызовов, пока не встретит блок try/catch. Но с асинхронным кодом всё не так очевидно.
Когда вы работаете с промисами, ошибки ловятся методом .catch(). Но как быть, если вы используете синтаксис async/await? Можно ли просто обернуть вызов асинхронной функции в try/catch? Как ловить ошибки, если их несколько? А если ошибка произошла в середине цепочки?
Давайте разбираться!
try/catch с async/await: базовый синтаксис
Вот как выглядит обработка ошибок в асинхронной функции с помощью try/catch:
async function fetchUser() {
try {
const response = await fetch('https://api.github.com/users/octocat');
if (!response.ok) {
throw new Error('Сервер вернул ошибку: ' + response.status);
}
const user = await response.json();
console.log('Пользователь:', user.login);
} catch (error) {
console.error('Произошла ошибка при загрузке пользователя:', error.message);
}
}
Как это работает:
- Всё, что находится внутри блока try, выполняется "по очереди": сначала fetch, потом response.json().
- Если где-то в этом процессе возникает ошибка (например, сеть недоступна, или сервер вернул ошибку, или что-то пошло не так при разборе JSON), выполнение сразу "перепрыгивает" в блок catch.
- В блоке catch вы получаете объект ошибки (error), с которым можно сделать что угодно: вывести на экран, залогировать, повторить попытку и т.д.
2. Пример: асинхронная функция с ошибкой
Давайте рассмотрим классический пример — загрузка данных с сервера, который иногда бывает недоступен.
async function getJoke() {
try {
const response = await fetch('https://api.chucknorris.io/jokes/random');
// Проверяем, всё ли ок
if (!response.ok) {
throw new Error('Не удалось получить шутку: ' + response.status);
}
const data = await response.json();
console.log(data.value);
} catch (err) {
console.log('Ошибка при получении шутки:', err.message);
}
}
getJoke();
Что произойдёт, если сервер недоступен или вернёт ошибку?
- Если сервер не отвечает, то fetch выбросит ошибку, и выполнение сразу попадёт в catch.
- Если сервер отвечает, но возвращает, например, 404, мы сами выбрасываем ошибку через throw new Error(...).
- Если всё хорошо, выводим шутку.
3. Возвращаемое значение из async-функции и try/catch
Что возвращает async-функция? Всегда — промис. Если внутри функции произошла ошибка и она не была обработана внутри, то промис будет отклонён (rejected) с этой ошибкой.
Пример:
async function brokenFunction() {
throw new Error('Что-то сломалось!');
}
brokenFunction()
.then(() => console.log('Всё хорошо!'))
.catch(err => console.log('Ошибка:', err.message));
Вывод:
Ошибка: Что-то сломалось!
Если ошибка была обработана внутри функции (например, с помощью try/catch), промис будет успешно выполнен (resolved), и ошибка наружу не выйдет.
4. try/catch вокруг await: ловим только ошибки из await
Иногда нужно обработать только ошибку от одной конкретной асинхронной операции. Для этого можно обернуть в try/catch только этот участок кода.
async function partialTryCatch() {
let user;
try {
const response = await fetch('https://api.github.com/users/octocat');
user = await response.json();
} catch (err) {
console.log('Ошибка при загрузке пользователя:', err.message);
user = { login: 'Гость' };
}
console.log('Имя пользователя:', user.login);
}
partialTryCatch();
Зачем так делать?
Если у вас несколько независимых асинхронных операций, и вы хотите для каждой из них реализовать свою логику обработки ошибок, не обязательно оборачивать всю функцию в один большой try/catch.
5. try/catch в "верхнем уровне" (top-level await)
В современных версиях Node.js и в модулях ES можно использовать await вне функций — на верхнем уровне файла. Ошибки в таком случае тоже можно ловить через try/catch:
try {
const response = await fetch('https://api.github.com/users/octocat');
const user = await response.json();
console.log(user.login);
} catch (err) {
console.error('Ошибка на верхнем уровне:', err.message);
}
Важно:
В обычных скриптах (не модулях) такой код не сработает — await вне функции использовать нельзя. Но в модулях (type="module") или в Node.js с поддержкой ESM — можно.
6. Пример из жизни: обработка ошибок в цепочке асинхронных действий
Давайте представим, что у нас есть простое приложение: пользователь вводит имя города, а мы показываем ему погоду. Для этого нужно:
- Отправить запрос на сервер погоды.
- Обработать ответ.
- Если что-то пошло не так — вывести пользователю сообщение.
async function getWeather(city) {
try {
const response = await fetch(`https://wttr.in/${city}?format=j1`);
if (!response.ok) {
throw new Error('Не удалось получить погоду');
}
const data = await response.json();
console.log(`Погода в городе ${city}: ${data.current_condition[0].temp_C}°C`);
} catch (err) {
console.log(`Ошибка при получении погоды для города "${city}":`, err.message);
}
}
getWeather('Moscow');
7. Генерация собственных ошибок внутри async-функций
Иногда нужно не только ловить ошибки, но и самому их выбрасывать, если, например, пришли некорректные данные или не выполнено какое-то условие.
async function divide(a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new TypeError('Оба аргумента должны быть числами!');
}
if (b === 0) {
throw new Error('Деление на ноль запрещено!');
}
return a / b;
}
async function main() {
try {
let result = await divide(10, 0);
console.log('Результат:', result);
} catch (err) {
console.error('Ошибка:', err.message);
}
}
main();
Обратите внимание:
- Если ошибка выброшена через throw, она тут же попадает в ближайший catch.
- Можно выбрасывать как стандартные ошибки (Error, TypeError), так и свои.
8. try/catch и параллельные await
Если вы делаете несколько асинхронных операций параллельно с помощью Promise.all, ошибка в любом из промисов приведёт к отклонению всего промиса. Ловить ошибку нужно через try/catch вокруг await Promise.all(...):
async function loadAll() {
try {
const [user, posts] = await Promise.all([
fetch('https://api.github.com/users/octocat').then(r => r.json()),
fetch('https://jsonplaceholder.typicode.com/posts/1').then(r => r.json())
]);
console.log('User:', user.login);
console.log('Post:', posts.title);
} catch (err) {
console.error('Ошибка при загрузке данных:', err.message);
}
}
loadAll();
9. Типичные ошибки при работе с try/catch и async/await
Ошибка №1: забыли await внутри try/catch
Очень частая ошибка — написать так:
async function oops() {
try {
fetch('https://api.github.com/users/octocat');
// ошибка не поймается catch-ем, если промис завершится с ошибкой!
} catch (err) {
console.error('Ошибка:', err.message);
}
}
Здесь fetch возвращает промис, но мы не используем await, поэтому ошибка внутри промиса не будет поймана этим catch. Всегда используйте await для асинхронных операций внутри try/catch!
Ошибка №2: try/catch не ловит синтаксические ошибки вне async-функции
try {
async function foo() {
await fetch('broken-url');
}
foo();
} catch (err) {
// Этот catch не поймает ошибку из async-функции!
}
Здесь ошибка произойдёт внутри промиса, и внешний catch её не увидит. Ошибку нужно ловить либо внутри async-функции через try/catch, либо через .catch() у промиса:
foo().catch(err => { /* обработка */ });
Ошибка №3: забыли обработать ошибку вообще
Если не обработать ошибку из async-функции, она "всплывёт" и может привести к некрасивому сообщению в консоли или даже завершению работы Node.js-приложения. Всегда оборачивайте await в try/catch или добавляйте .catch() у промиса!
Ошибка №4: ловим слишком много или слишком мало
Иногда новички оборачивают в try/catch слишком большой участок кода, и не могут понять, где именно возникла ошибка. Лучше ловить только то, что действительно может "упасть", и делать обработку ошибок максимально локальной.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ