Как правильно отклонить (reject) промис в async/await синтаксисе в TypeScript?
Правильный способ: использование throw
В async функциях отклонение промиса (reject) осуществляется через механизм исключений с помощью оператора throw. Это не баг, а фича! Когда вы выбрасываете исключение внутри async функции, оно автоматически преобразуется в отклонённый промис (rejected promise). TypeScript правильно типизирует это поведение.
Вот как должен выглядеть ваш исправленный код:
async function fetchUserData(userId: string): Promise<UserData> {
if (!userId) {
// Правильный способ: throw создаст rejected promise
throw new Error('User ID is required');
}
const response = await fetch(`/api/users/${userId}`);
// Проверяем HTTP статус
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json() as Promise<UserData>;
}
// Использование:
try {
const data = await fetchUserData('123');
console.log('Данные получены:', data);
} catch (error) {
// error — это то, что мы выбросили через throw
if (error instanceof Error) {
console.error('Ошибка:', error.message);
}
}
Почему throw, а не return Promise.reject()?
- Консистентность: В async функциях throw — это единственный способ синхронно завершить выполнение и вернуть отклонённый промис
- Стек вызовов: throw сохраняет правильный стек вызовов для отладки
- Типизация: TypeScript корректно выводит типы через catch
Типизация ошибок в TypeScript
Для лучшей типизации вы можете создать кастомные классы ошибок:
class ValidationError extends Error {
constructor(message: string, public code: number) {
super(message);
this.name = 'ValidationError';
}
}
class ApiError extends Error {
constructor(message: string, public statusCode: number) {
super(message);
this.name = 'ApiError';
}
}
async function fetchUserData(userId: string): Promise<UserData> {
if (!userId) {
throw new ValidationError('User ID is required', 400);
}
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new ApiError('Failed to fetch user', response.status);
}
return response.json() as Promise<UserData>;
}
// Использование с обработкой разных типов ошибок:
try {
const data = await fetchUserData('123');
} catch (error) {
if (error instanceof ValidationError) {
console.log(`Ошибка валидации [${error.code}]: ${error.message}`);
} else if (error instanceof ApiError) {
console.log(`API ошибка [${error.statusCode}]: ${error.message}`);
} else {
console.log('Неизвестная ошибка:', error);
}
}
Когда использовать Promise.reject()?
Promise.reject() имеет смысл только вне async функций или в конструкторе Promise:
// В конструкторе Promise
const promise = new Promise<string>((resolve, reject) => {
reject(new Error('Something went wrong'));
});
// В цепочке then/catch (но не в async)
somePromise
.then(value => {
if (value === null) {
return Promise.reject(new Error('Value is null'));
}
return value;
});
Внутри async функции return Promise.reject() будет работать, но это избыточно и менее эффективно, чем throw. Кроме того, throw автоматически останавливает выполнение функции, в то время как return Promise.reject() продолжит выполнение, если не использовать return.
Важно про тип возвращаемого значения
Когда вы используете throw, TypeScript всё равно ожидает, что функция вернёт Promise<UserData>. Если вы выбрасываете ошибку, функция всё равно возвращает Promise, просто в отклонённом состоянии. Тип возвращаемого значения не меняется — это всё ещё Promise<UserData>, но в отклонённом состоянии он содержит ошибку, а не UserData.
<<Альтернативный подход: возврат Result-типа (Either monad)
Некоторые разработчики предпочитают не использовать исключения для управления потоком и вместо этого возвращают специальный тип, который может содержать либо успешный результат, либо ошибку. Это особенно популярно в функциональном программировании.
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
async function fetchUserData(userId: string): Promise<Result<UserData>> {
if (!userId) {
return {
success: false,
error: new Error('User ID is required')
};
}
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
return {
success: false,
error: new Error(`HTTP error! status: ${response.status}`)
};
}
const data = await response.json() as UserData;
return { success: true, data };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error : new Error('Unknown error')
};
}
}
// Использование:
const result = await fetchUserData('123');
if (result.success) {
console.log('Данные:', result.data);
} else {
console.error('Ошибка:', result.error.message);
}
Преимущества: явная обработка ошибок без try/catch, хорошая типизация, предсказуемое поведение. Недостатки: более многословный код, необходимость всегда проверять success флаг, потеря возможности использовать стандартные Promise методы цепочек.
Для TypeScript существуют библиотеки, реализующие этот подход, например neverthrow или fp-ts с типом Either.
Ответов пока нет.
Войдите чтобы оставить ответ
Блоги
Топ ошибок начинающих C# разработчиков и как их избежать