Пошук уроків, статей та іншого контенту
Розберете перехоплення помилок в async-функціях і правильне поєднання async/await із конструкцією try...catch.
async-функціяхasync/await робить асинхронний код схожим на синхронний. Це особливо зручно для обробки помилок: замість ланцюжка .catch() можна використовувати звичайну конструкцію try...catch.
Будь-яка async-функція завжди повертає Promise:
async function getValue() {
return 42;
}
getValue().then((value) => {
console.log(value); // 42
});Якщо всередині async-функції виникає помилка або виконується throw, повернений Promise переходить у стан rejected.
async function fail() {
throw new Error("Щось пішло не так");
}
fail().catch((error) => {
console.error(error.message); // Щось пішло не так
});Отже:
return в async-функції створює виконаний (fulfilled) Promise;
throw створює відхилений (rejected) Promise;
помилка під час await поводиться так, ніби вона виникла в поточному синхронному коді.
try...catchБазовий спосіб перехопити помилку в async-функції:
async function loadData() {
try {
const response = await fetch("https://jsonplaceholder.typicode.com/todos/1");
if (!response.ok) {
throw new Error(`HTTP-помилка: ${response.status}`);
}
const data = await response.json();
console.log(data);
} catch (error) {
console.error("Не вдалося завантажити дані:", error.message);
}
}
loadData();Блок try містить код, який може завершитися помилкою. Якщо помилка виникає:
виконання блоку try припиняється;
керування переходить до блоку catch;
об'єкт помилки стає доступним у змінній error.
У прикладі перевіряється response.ok, оскільки fetch не відхиляє Promise автоматично для HTTP-статусів 404 або 500. Для fetch мережевою помилкою є, наприклад, недоступність сервера або проблема з підключенням, але HTTP-помилку потрібно перевіряти самостійно.
awaitОператор await очікує завершення Promise. Якщо цей Promise відхилено, await викидає помилку.
function getNumber() {
return Promise.reject(new Error("Не вдалося отримати число"));
}
async function processNumber() {
try {
const number = await getNumber();
console.log(number);
} catch (error) {
console.error("Помилка під час обробки:", error.message);
}
}
processNumber();У цьому випадку catch перехоплює відхилення Promise, поверненого функцією getNumber.
Так само перехоплюються помилки з кількох послідовних операцій:
async function loadUserProfile() {
try {
const userResponse = await fetch(
"https://jsonplaceholder.typicode.com/users/1"
);
if (!userResponse.ok) {
throw new Error("Не вдалося завантажити користувача");
}
const user = await userResponse.json();
const postsResponse = await fetch(
`https://jsonplaceholder.typicode.com/posts?userId=${user.id}`
);
if (!postsResponse.ok) {
throw new Error("Не вдалося завантажити публікації");
}
const posts = await postsResponse.json();
return { user, posts };
} catch (error) {
console.error("Помилка завантаження профілю:", error.message);
return null;
}
}
loadUserProfile().then((profile) => {
if (profile) {
console.log(profile.user.name);
console.log(`Кількість публікацій: ${profile.posts.length}`);
}
});Помилка на будь-якому етапі передасть керування до одного спільного catch.
catch має отримати об'єкт помилкиЗазвичай у catch використовують об'єкт Error:
try {
throw new Error("Некоректні дані");
} catch (error) {
console.log(error.name); // Error
console.log(error.message); // Некоректні дані
console.log(error.stack); // Стек викликів
}Найважливіші властивості:
name — тип помилки;
message — повідомлення;
stack — стек викликів, корисний під час налагодження.
У сучасному JavaScript технічно можна виконати throw з будь-яким значенням:
throw "Помилка";
throw 404;
throw { code: "NETWORK_ERROR" };Проте в прикладному коді краще викидати екземпляри Error або його підкласів. Так помилки мають передбачувану структуру.
throw new Error("Операція недоступна");Для різних ситуацій можна створювати власні класи помилок або перевіряти властивості об'єкта помилки.
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
}
function validateUsername(username) {
if (typeof username !== "string" || username.length < 3) {
throw new ValidationError(
"Ім'я користувача має містити щонайменше 3 символи"
);
}
return true;
}
async function registerUser(username) {
try {
validateUsername(username);
return { username };
} catch (error) {
if (error instanceof ValidationError) {
console.error("Помилка валідації:", error.message);
return null;
}
throw error;
}
}
registerUser("ab").then((user) => {
console.log(user); // null
});Якщо помилка не належить до очікуваного типу, її можна повторно викинути за допомогою throw error. Це не дозволяє випадково приховати програмні помилки.
async function saveSettings(settings) {
try {
if (!settings.theme) {
throw new Error("Тему не вказано");
}
// Уявна операція збереження
return { saved: true };
} catch (error) {
if (error.message === "Тему не вказано") {
return { saved: false, reason: "invalid-settings" };
}
// Невідома помилка має бути передана вище
throw error;
}
}try...catchНе обов'язково поміщати всю функцію в один великий блок try. Краще охоплювати лише операції, для яких ви справді знаєте спосіб обробки помилки.
async function loadSettings() {
const response = await fetch("/settings");
if (!response.ok) {
throw new Error("Сервер не повернув налаштування");
}
const settings = await response.json();
try {
return JSON.parse(settings.layout);
} catch (error) {
console.error("Некоректний формат layout");
return {};
}
}У цьому прикладі:
помилка мережі або сервера передається виклику вищого рівня;
помилка розбору layout обробляється локально, оскільки для неї є запасне значення {}.
Якщо обробляти всі помилки однаково, можна втратити важливу інформацію про причину проблеми.
finallyБлок finally виконується незалежно від того, чи виникла помилка:
async function loadProducts() {
let isLoading = true;
try {
const response = await fetch(
"https://dummyjson.com/products"
);
if (!response.ok) {
throw new Error(`HTTP-помилка: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error("Помилка завантаження товарів:", error.message);
return null;
} finally {
isLoading = false;
console.log("Завантаження завершено:", isLoading);
}
}
loadProducts();finally зазвичай використовують для:
приховування індикатора завантаження;
закриття з'єднання;
очищення тимчасових ресурсів;
розблокування елементів інтерфейсу.
finally виконується навіть тоді, коли в try є return:
async function example() {
try {
return "результат";
} finally {
console.log("Цей код все одно виконається");
}
}
example().then(console.log);Не варто повертати інше значення з finally, оскільки це може перекрити результат або помилку з try чи catch:
function dangerousExample() {
try {
throw new Error("Важлива помилка");
} finally {
return "помилка прихована";
}
}
console.log(dangerousExample()); // "помилка прихована"Такий код ускладнює налагодження, тому return у finally краще не використовувати.
try...catch можна розташувати не всередині функції, а навколо її виклику:
async function getCurrentUser() {
const response = await fetch("/api/user");
if (!response.ok) {
throw new Error("Користувача не знайдено");
}
return response.json();
}
async function renderPage() {
try {
const user = await getCurrentUser();
console.log(`Вітаємо, ${user.name}!`);
} catch (error) {
console.error("Не вдалося відобразити сторінку:", error.message);
}
}
renderPage();Це корисно, коли функція повинна лише виконати свою відповідальність і передати помилку тому, хто вирішить, що з нею робити.
Наприклад, функція доступу до даних може не показувати повідомлення користувачу. Вона лише повідомляє про проблему:
async function getProducts() {
const response = await fetch("/api/products");
if (!response.ok) {
throw new Error("Помилка сервера");
}
return response.json();
}
async function showProducts() {
try {
const products = await getProducts();
console.log(products);
} catch (error) {
console.error("Покажіть користувачу повідомлення про помилку");
console.error(error.message);
}
}Іноді після помилки програма може продовжити роботу із запасним значенням:
async function getFeatureFlags() {
try {
const response = await fetch("/api/feature-flags");
if (!response.ok) {
throw new Error("Не вдалося завантажити прапорці");
}
return await response.json();
} catch (error) {
console.error(error.message);
return {
newDashboard: false,
experimentalSearch: false
};
}
}Такий підхід доречний, якщо:
запасне значення справді безпечне;
функція має передбачуваний контракт;
помилка все одно записується в журнал або повідомляється іншим способом.
Не слід повертати null, undefined або порожній масив без пояснення, якщо це може замаскувати проблему. Викликач повинен розуміти, що означає результат.
Якщо операції залежать одна від одної, їх виконують послідовно:
async function loadOrder() {
try {
const orderResponse = await fetch("/api/order/10");
if (!orderResponse.ok) {
throw new Error("Не вдалося завантажити замовлення");
}
const order = await orderResponse.json();
const customerResponse = await fetch(
`/api/customers/${order.customerId}`
);
if (!customerResponse.ok) {
throw new Error("Не вдалося завантажити клієнта");
}
const customer = await customerResponse.json();
return { order, customer };
} catch (error) {
console.error(error.message);
return null;
}
}Якщо операції незалежні, їх краще запускати паралельно за допомогою Promise.all:
async function loadDashboard() {
try {
const [productsResponse, categoriesResponse] = await Promise.all([
fetch("/api/products"),
fetch("/api/categories")
]);
if (!productsResponse.ok || !categoriesResponse.ok) {
throw new Error("Не вдалося завантажити дані панелі");
}
const [products, categories] = await Promise.all([
productsResponse.json(),
categoriesResponse.json()
]);
return { products, categories };
} catch (error) {
console.error("Помилка завантаження панелі:", error.message);
return null;
}
}Promise.all відхиляється, якщо відхилився хоча б один із переданих Promise. Це зручно, коли всі результати потрібні для подальшої роботи.
Якщо потрібно отримати результат кожної операції незалежно від помилок, використовуйте Promise.allSettled:
async function loadOptionalData() {
const results = await Promise.allSettled([
fetch("/api/news"),
fetch("/api/recommendations"),
fetch("/api/ads")
]);
for (const result of results) {
if (result.status === "fulfilled") {
console.log("Операцію виконано");
} else {
console.error("Операція завершилася помилкою:", result.reason);
}
}
return results;
}try...catch і await у циклахЯкщо кожен елемент потрібно обробити незалежно, try...catch можна розташувати всередині циклу:
async function processUsers(userIds) {
const processedUsers = [];
for (const userId of userIds) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`Користувача ${userId} не знайдено`);
}
const user = await response.json();
processedUsers.push(user);
} catch (error) {
console.error(`Помилка для ID ${userId}:`, error.message);
}
}
return processedUsers;
}У цьому випадку помилка одного користувача не зупиняє обробку наступних.
Якщо ж помилка має припинити весь процес, try...catch можна розташувати навколо циклу:
async function processAllUsers(userIds) {
try {
const users = [];
for (const userId of userIds) {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`Помилка завантаження користувача ${userId}`);
}
users.push(await response.json());
}
return users;
} catch (error) {
console.error("Процес припинено:", error.message);
return [];
}
}Вибір залежить від вимог програми: продовжувати процес після окремої помилки чи завершувати його повністю.
awaitЯкщо не використати await, try...catch не перехопить асинхронну помилку:
async function loadData() {
try {
fetch("/api/data"); // Помилка може залишитися неперехопленою
} catch (error) {
console.error(error);
}
}Правильні варіанти:
async function loadData() {
try {
await fetch("/api/data");
} catch (error) {
console.error(error);
}
}Або:
function loadData() {
return fetch("/api/data").catch((error) => {
console.error(error);
});
}Порожній catch приховує проблему:
try {
await loadData();
} catch (error) {
// Небезпечно: причина помилки втрачена
}Якщо помилку неможливо обробити на цьому рівні, її потрібно передати вище:
try {
await loadData();
} catch (error) {
throw error;
}Або додати контекст:
try {
await loadData();
} catch (error) {
throw new Error("Не вдалося завантажити сторінку", {
cause: error
});
}Властивість cause дає змогу зберегти початкову причину помилки.
fetchfetch не вважає HTTP-статуси 404 і 500 помилкою Promise. Потрібно перевіряти response.ok:
const response = await fetch("/api/data");
if (!response.ok) {
throw new Error(`HTTP-помилка: ${response.status}`);
}tryКоли весь код функції міститься в одному великому try, помилки різного походження обробляються однаково. Це може приховати помилку в логіці або помилково повернути запасне значення.
Краще обробляти локально лише очікувані помилки, а невідомі передавати на вищий рівень.
.catch() і try...catch без потребиТакий код працює, але його складніше читати:
async function loadData() {
try {
const data = await fetch("/api/data").catch((error) => {
console.error(error);
throw error;
});
return data;
} catch (error) {
console.error("Друга обробка тієї самої помилки");
}
}Зазвичай достатньо одного підходу:
async function loadData() {
try {
const response = await fetch("/api/data");
return response;
} catch (error) {
console.error(error);
return null;
}
}async-функціїНавіть якщо всередині функції немає try...catch, помилку можна перехопити під час виклику:
async function main() {
await loadData();
}
main().catch((error) => {
console.error("Неперехоплена помилка:", error);
});Якщо відхилений Promise не має обробника, середовище може повідомити про неперехоплену помилку.
Один із поширених шаблонів для асинхронної операції:
async function getUser(userId) {
try {
const response = await fetch(
`https://jsonplaceholder.typicode.com/users/${userId}`
);
if (!response.ok) {
throw new Error(`Користувача не знайдено: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error("getUser:", error.message);
// Очікувану помилку перетворюємо на зрозумілий результат
return null;
}
}
async function main() {
try {
const user = await getUser(1);
if (!user) {
console.log("Не вдалося отримати дані користувача");
return;
}
console.log(`Ім'я: ${user.name}`);
console.log(`Електронна пошта: ${user.email}`);
} catch (error) {
// Сюди потрапляють помилки, які getUser не обробила
console.error("Критична помилка:", error.message);
}
}
main();У цьому прикладі:
getUser перевіряє HTTP-відповідь;
очікувану помилку перетворює на null;
main перевіряє результат;
невідомі або критичні помилки можуть піднятися до зовнішнього catch.
async-функція завжди повертає Promise.
throw в async-функції створює відхилений Promise.
Помилки від await перехоплюються конструкцією try...catch.
Блок finally виконується незалежно від результату операції.
HTTP-помилки fetch потрібно перевіряти через response.ok.
Очікувані помилки можна обробити локально та повернути запасне значення.
Невідомі помилки не слід приховувати — їх потрібно передавати вище.
Для незалежних операцій використовуйте Promise.all або Promise.allSettled залежно від потрібної поведінки.
Не залишайте порожні catch і не повертайте значення з finally, якщо це може приховати помилку.