Как правильно reject в async/await? Разбор на TypeScript

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Async/Await и reject: почему это боль для новичков?

Вы когда-нибудь писали async функцию, ждали ответа от сервера, а потом ловили UnhandledPromiseRejection? Знакомо? Это одна из самых частых проблем, с которой сталкиваются разработчики, переходящие на async/await в TypeScript.

На Stack Overflow вопрос "How to reject in async/await syntax?" набрал тысячи просмотров. И не зря. На первый взгляд кажется, что достаточно просто вернуть Promise.reject() — и дело в шляпе. Но в реальных проектах всё сложнее: типы, цепочки вызовов, потеря контекста ошибок.

В этом руководстве мы разберём, как правильно отклонять промисы в async функциях TypeScript, научимся отличать throw от return Promise.reject() и посмотрим на практические примеры из боевых проектов.

Основы: как работает reject в async/await

Прежде чем нырять в дебри, давайте вспомним матчасть. Любая async функция в TypeScript возвращает Promise. Если внутри неё происходит throw или возвращается Promise.reject(), промис переходит в состояние rejected.

// Простейший пример reject в async/await
async function fetchUser(id: number): Promise<User> {
    if (id <= 0) {
        throw new Error('Invalid user ID');
    }
    // ...логика получения пользователя
}

Здесь throw автоматически превращается в reject. Но что если мы хотим вернуть уже готовый отклонённый промис?

throw vs return Promise.reject()

В TypeScript оба подхода работают, но есть нюансы:

  • throw — более идиоматичный способ. Он останавливает выполнение функции и передаёт управление в ближайший catch.
  • return Promise.reject() — явно возвращает отклонённый промис. Полезно, когда нужно передать ошибку из другой функции.
// Разница между throw и return Promise.reject()
async function processOrder(orderId: string): Promise<OrderStatus> {
    // Вариант 1: throw
    if (!orderId) {
        throw new Error('Order ID is required');
    }
    
    // Вариант 2: return Promise.reject
    if (orderId.startsWith('test_')) {
        return Promise.reject(new Error('Test orders are not allowed'));
    }
    
    return { status: 'processing' };
}

Важно: TypeScript не всегда корректно выводит типы при return Promise.reject(). Если функция ожидает Promise<T>, а вы возвращаете Promise.reject(), может возникнуть ошибка типизации. В таких случаях лучше использовать throw.

Типичные ошибки при reject в async функциях

Даже опытные разработчики на TypeScript иногда допускают классические ошибки. Вот топ-3:

1. Забыли обработать reject в вызывающем коде

async function main() {
    // Плохо: нет try/catch
    const user = await fetchUser(-1);
    console.log(user); // UnhandledPromiseRejection!
}

// Хорошо:
async function mainSafe() {
    try {
        const user = await fetchUser(-1);
        console.log(user);
    } catch (error) {
        console.error('Failed to fetch user:', error);
    }
}

2. Неправильная типизация ошибок

В TypeScript catch блок по умолчанию ловит unknown. Это правильно, но иногда разработчики пытаются обращаться к error.message без проверки.

try {
    await riskyOperation();
} catch (error) {
    // Ошибка: error — unknown
    console.log(error.message); // ❌ Property 'message' does not exist on type 'unknown'
    
    // Правильно:
    if (error instanceof Error) {
        console.log(error.message);
    } else {
        console.log('Unknown error:', error);
    }
}

3. Смешивание reject и resolve в одной функции

Иногда разработчики пытаются одновременно и отклонить, и выполнить промис:

// Антипаттерн
async function getData(): Promise<Data> {
    try {
        const result = await apiCall();
        return result; // resolve
    } catch (error) {
        return Promise.reject(error); // reject
    }
}

// Лучше:
async function getDataBetter(): Promise<Data> {
    return await apiCall();
}

Продвинутые техники: reject с кастомными ошибками

В реальных проектах просто new Error('message') часто недостаточно. Нужно передавать код ошибки, данные, стек вызовов. В TypeScript для этого создают кастомные классы ошибок.

// Кастомная ошибка с дополнительными полями
class ApiError extends Error {
    constructor(
        public statusCode: number,
        public message: string,
        public details?: Record<string, unknown>
    ) {
        super(message);
        this.name = 'ApiError';
    }
}

async function fetchData(url: string): Promise<ResponseData> {
    const response = await fetch(url);
    
    if (!response.ok) {
        throw new ApiError(
            response.status,
            `HTTP Error: ${response.statusText}`,
            { url, timestamp: Date.now() }
        );
    }
    
    return response.json();
}

Такой подход позволяет в catch блоке точно определить тип ошибки и обработать её специфично:

try {
    const data = await fetchData('/api/users');
} catch (error) {
    if (error instanceof ApiError) {
        if (error.statusCode === 404) {
            showNotFoundPage();
        } else if (error.statusCode === 500) {
            showServerError(error.details);
        }
    } else {
        showGenericError();
    }
}

Асинхронные цепочки: как не потерять reject

Когда у вас несколько async вызовов подряд, легко потерять reject где-то посередине. Рассмотрим типичный сценарий:

async function processPayment(paymentId: string): Promise<PaymentResult> {
    // Шаг 1: проверка платежа
    const payment = await getPayment(paymentId);
    if (!payment) {
        throw new Error('Payment not found');
    }
    
    // Шаг 2: списание средств
    const chargeResult = await chargeCard(payment);
    if (!chargeResult.success) {
        // Здесь reject пойдёт вверх по цепочке
        throw new PaymentError('Charge failed', chargeResult.errorCode);
    }
    
    // Шаг 3: отправка уведомления
    await sendNotification(payment.userId);
    
    return { status: 'completed', transactionId: chargeResult.id };
}

Обратите внимание: если chargeCard выбросит ошибку, выполнение сразу перейдёт в catch вызывающего кода. sendNotification не выполнится. Это правильное поведение, но иногда нужно выполнить очистку ресурсов.

finally: гарантированное выполнение после reject

async function processWithCleanup(orderId: string): Promise<void> {
    const dbConnection = await openConnection();
    try {
        const order = await dbConnection.findOrder(orderId);
        if (!order) {
            throw new Error('Order not found');
        }
        await processOrder(order);
    } catch (error) {
        console.error('Processing failed:', error);
        throw error; // П                

Блоги

Рекомендаций по книгам