Пошук уроків, статей та іншого контенту
Навчитеся обробляти відхилені проміси за допомогою catch, finally та обробників помилок у ланцюжках then.
Проміс може завершитися одним із двох способів:
виконанням — fulfilled;
відхиленням — rejected.
Для обробки відхилених промісів використовують:
другий аргумент методу then;
метод catch;
метод finally для дій, які мають виконуватися незалежно від результату.
Відхилення автоматично передається далі ланцюжком, доки не буде оброблене.
Promise.resolve("Початок")
.then((value) => {
console.log(value);
throw new Error("Щось пішло не так");
})
.then((value) => {
// Цей обробник не виконається,
// тому що попередній then завершився помилкою.
console.log(value);
})
.catch((error) => {
console.error("Помилка:", error.message);
});Результат:
Початок
Помилка: Щось пішло не такКоли всередині обробника then виникає виняток або викликається reject, поточний проміс стає відхиленим. Наступний обробник успішного результату пропускається, а помилка переходить до найближчого обробника помилок.
catchМетод catch призначений для обробки відхилення:
promise.catch((error) => {
console.error(error);
});Фактично catch є скороченим записом для then із першим аргументом null:
promise.catch(handleError);
// Еквівалентний запис
promise.then(null, handleError);Зазвичай catch читати простіше, тому саме його використовують для обробки помилок у ланцюжках.
function getUser() {
return Promise.reject(new Error("Не вдалося отримати користувача"));
}
getUser()
.then((user) => {
console.log("Користувач:", user.name);
return user;
})
.then((user) => {
console.log("Ідентифікатор:", user.id);
return user.id;
})
.catch((error) => {
console.error("Помилка під час завантаження даних:", error.message);
});Якщо getUser() відхилиться, усі наступні обробники успішного результату будуть пропущені. Помилку перехопить catch.
Розглянемо ланцюжок із кількома обробниками:
Promise.resolve()
.then(() => {
throw new Error("Помилка на першому кроці");
})
.then(
() => {
console.log("Цей код не виконається");
},
(error) => {
console.log("Помилку оброблено:", error.message);
}
)
.then(() => {
console.log("Ланцюжок продовжується");
});У цьому прикладі другий аргумент першого then обробляє помилку. Після цього його обробник повертає успішний результат undefined, тому наступний then виконується.
Якщо обробник помилки нічого не повертає, вважається, що помилка була оброблена й проміс успішно завершився.
then і catch — не завжди взаємозамінніУ методі then можна передати два обробники:
promise.then(
(value) => {
console.log("Успіх:", value);
},
(error) => {
console.error("Помилка:", error);
}
);Але другий аргумент then обробляє лише відхилення попереднього промісу. Він не перехоплює помилки, які виникли в першому обробнику цього самого then.
Promise.resolve("Дані").then(
() => {
throw new Error("Помилка всередині обробника");
},
(error) => {
// Цей обробник не виконається.
console.error("Помилка:", error.message);
}
);Помилка з першого обробника перейде до наступного catch:
Promise.resolve("Дані")
.then(
() => {
throw new Error("Помилка всередині обробника");
},
(error) => {
console.error("Помилка попереднього промісу:", error.message);
}
)
.catch((error) => {
console.error("Перехоплено через catch:", error.message);
});Тому для обробки всіх помилок у ланцюжку часто використовують catch наприкінці:
Promise.resolve("Дані")
.then(() => {
throw new Error("Помилка всередині обробника");
})
.catch((error) => {
console.error("Перехоплено:", error.message);
});Обробник catch може повернути звичайне значення або новий проміс. У такому разі ланцюжок продовжиться як успішний.
Promise.reject(new Error("Основний запит не вдався"))
.catch((error) => {
console.error(error.message);
// Повертаємо запасні дані.
return { source: "cache", value: 42 };
})
.then((data) => {
console.log("Отримано дані:", data);
});Результат:
Основний запит не вдався
Отримано дані: { source: 'cache', value: 42 }Цей підхід називають відновленням після помилки. Він доречний, коли застосунок має запасний сценарій:
використати дані з кешу;
показати значення за замовчуванням;
повторити операцію;
повернути порожній список.
Якщо в catch знову кинути помилку, ланцюжок залишиться відхиленим:
Promise.reject(new Error("Помилка сервера"))
.catch((error) => {
console.error("Локальне журналювання:", error.message);
throw new Error("Не вдалося отримати дані з резервного джерела");
})
.catch((error) => {
console.error("Фінальна помилка:", error.message);
});Так можна обробити помилку на одному рівні, але передати її далі для остаточного рішення.
Іноді помилку потрібно обробити якомога ближче до операції, яка її спричинила. Наприклад, можна використати кеш лише для помилки завантаження, але не для помилки форматування даних.
function loadFromServer() {
return Promise.reject(new Error("Сервер недоступний"));
}
function loadFromCache() {
return Promise.resolve({ id: 1, name: "Користувач із кешу" });
}
loadFromServer()
.catch((error) => {
console.error("Основне джерело недоступне:", error.message);
return loadFromCache();
})
.then((user) => {
if (!user.name) {
throw new Error("У користувача відсутнє ім'я");
}
return user.name;
})
.then((name) => {
console.log("Ім'я:", name);
})
.catch((error) => {
console.error("Не вдалося завершити операцію:", error.message);
});Перший catch відновлює операцію за допомогою кешу. Останній catch обробляє помилки, які могли виникнути під час наступних кроків.
finallyfinally виконується незалежно від того, завершився проміс успішно чи з помилкою:
promise.finally(() => {
// Код виконується в обох випадках.
});Найчастіше finally використовують для:
приховування індикатора завантаження;
звільнення ресурсу;
закриття з'єднання;
очищення тимчасових даних;
завершення спільних дій.
function loadData() {
return new Promise((resolve) => {
setTimeout(() => {
resolve(["JavaScript", "Promise"]);
}, 500);
});
}
console.log("Завантаження...");
loadData()
.then((items) => {
console.log("Отримано:", items);
})
.catch((error) => {
console.error("Помилка:", error.message);
})
.finally(() => {
console.log("Завантаження завершено");
});finally не отримує результат або помилку як аргумент. Якщо потрібно проаналізувати результат, використовуйте then або catch.
finally не змінює результат без потребиЗначення, повернуте з finally, зазвичай ігнорується:
Promise.resolve("Початкове значення")
.finally(() => {
return "Інше значення";
})
.then((value) => {
console.log(value);
});Результат:
Початкове значенняfinally зберігає попередній результат або попередню помилку.
Однак якщо всередині finally виникне помилка або буде повернено відхилений проміс, початковий результат буде замінено новою помилкою:
Promise.resolve("Успіх")
.finally(() => {
throw new Error("Помилка очищення");
})
.then(
(value) => {
console.log(value);
},
(error) => {
console.error(error.message);
}
);Результат:
Помилка очищенняТому код у finally також має бути надійним: помилка очищення може змінити результат усієї операції.
fetchfetch повертає проміс. Важливо пам'ятати, що HTTP-відповідь зі статусом 404 або 500 сама по собі не відхиляє проміс. Відхилення відбувається, наприклад, у разі мережевої помилки. Для перевірки HTTP-статусу потрібно зробити це явно через response.ok.
function loadPost() {
return fetch("https://jsonplaceholder.typicode.com/posts/1")
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP-помилка: ${response.status}`);
}
return response.json();
});
}
console.log("Початок завантаження");
loadPost()
.then((post) => {
console.log("Назва:", post.title);
})
.catch((error) => {
console.error("Не вдалося завантажити допис:", error.message);
})
.finally(() => {
console.log("Запит завершено");
});Послідовність роботи:
fetch повертає проміс із відповіддю.
then перевіряє HTTP-статус.
response.json() повертає інший проміс.
Наступний then отримує розібрані JSON-дані.
catch обробляє мережеві помилки, помилки статусу та помилки розбору JSON.
finally виконується в будь-якому випадку.
Для різних типів помилок можна створити окремі класи:
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
}
function validateUser(user) {
if (!user.email) {
throw new ValidationError("Поле email є обов'язковим");
}
return user;
}
Promise.resolve({ name: "Олена" })
.then(validateUser)
.catch((error) => {
if (error instanceof ValidationError) {
console.error("Помилка валідації:", error.message);
return { name: "Олена", email: "невідомо" };
}
throw error;
})
.then((user) => {
console.log("Користувач:", user);
})
.catch((error) => {
console.error("Непередбачена помилка:", error.message);
});Перевірка типу помилки дозволяє розділяти очікувані ситуації та справді непередбачені помилки.
У ланцюжку потрібно повертати проміс із кожного асинхронного кроку. Інакше наступний then не чекатиме завершення операції.
Неправильний варіант:
function loadUser() {
return Promise.resolve({ id: 1 });
}
function loadPosts(userId) {
return Promise.resolve([{ title: "Перший допис", userId }]);
}
loadUser()
.then((user) => {
loadPosts(user.id);
})
.then((posts) => {
console.log(posts);
});У цьому прикладі loadPosts викликається, але його проміс не повертається. Тому posts у наступному then матиме значення undefined.
Правильний варіант:
loadUser()
.then((user) => {
return loadPosts(user.id);
})
.then((posts) => {
console.log(posts);
})
.catch((error) => {
console.error("Помилка:", error.message);
});Стрілочний запис без фігурних дужок також автоматично повертає результат:
loadUser()
.then((user) => loadPosts(user.id))
.then((posts) => {
console.log(posts);
});Якщо відхилений проміс не має обробника catch, середовище виконання повідомить про неперехоплене відхилення:
Promise.reject(new Error("Необроблена помилка"));У браузері це зазвичай відображається в консолі. У Node.js поведінка залежить від версії та налаштувань середовища, але покладатися на це не слід.
Для кожного незалежного ланцюжка передбачайте обробник помилок:
performTask()
.then(handleSuccess)
.catch(handleError)
.finally(cleanup);Обробник помилки не обов'язково має бути безпосередньо в кінці всього коду, але він має охоплювати всі операції, помилки яких ви хочете обробити.
catch лише для синтаксичних помилокcatch у ланцюжку промісів обробляє не тільки мережеві помилки. До нього також потрапляють:
винятки, кинуті в then;
помилки під час розбору відповіді;
відхилені проміси, повернуті з обробників;
помилки, кинуті в іншому catch.
fetch не відхиляє проміс лише через статус 404 або 500. Перевіряйте response.ok вручну:
fetch("/api/data")
.then((response) => {
if (!response.ok) {
throw new Error(`Статус відповіді: ${response.status}`);
}
return response.json();
})
.catch((error) => {
console.error(error.message);
});Якщо catch нічого не повертає і не кидає нову помилку, наступний then вважатиме операцію успішною:
doSomething()
.catch((error) => {
console.error(error.message);
})
.then(() => {
// Цей код виконається, навіть якщо doSomething завершився помилкою.
});Якщо продовжувати ланцюжок як успішний сценарій не можна, повторно киньте помилку:
doSomething()
.catch((error) => {
console.error(error.message);
throw error;
})
.then(() => {
// Код не виконається, якщо помилка не була відновлена.
});finally для зміни результатуfinally призначений для завершальних дій, а не для підготовки нових даних. Значення краще повертати з then або catch.
return для вкладеного промісуБез return наступний обробник не чекатиме завершення асинхронної операції:
// Неправильно
.then(() => {
saveData();
})
// Правильно
.then(() => {
return saveData();
})Відхилення промісу передається наступним обробникам ланцюжка.
catch обробляє помилки попередніх операцій і винятки з then.
Другий аргумент then обробляє лише відхилення попереднього промісу, але не помилки у власному першому обробнику.
catch може відновити ланцюжок, якщо поверне значення або успішний проміс.
Повторне throw у catch передає помилку далі.
finally виконується незалежно від результату й використовується для очищення та завершальних дій.
Помилка всередині finally може замінити початковий результат.
Усі асинхронні операції в ланцюжку потрібно повертати через return.
Для fetch HTTP-статус потрібно перевіряти через response.ok.
Кожен незалежний ланцюжок має мати обробник помилок, щоб уникати неперехоплених відхилень.