Avatar ·

Как правильно отклонить (reject) промис в async/await синтаксисе в TypeScript?

Всем привет! Я начал изучать TypeScript и async/await, но столкнулся с проблемой. В документации сказано, что async функция всегда возвращает Promise. Когда я хочу успешно завершить промис, я просто возвращаю значение. Но как правильно отклонить промис (reject) внутри async функции? Вот пример, который у меня не работает как ожидалось: ```typescript async function fetchUserData(userId: string): Promise { if (!userId) { // Я хочу здесь отклонить промис с ошибкой return new Error('User ID is required'); // Так неправильно } const response = await fetch(`/api/users/${userId}`); return response.json(); } ``` Я пробовал использовать `throw new Error()`, но мне кажется это странным, ведь throw обычно для исключений, а не для отклонения промисов. Также я видел в старых примерах использование `Promise.reject()`, но не понимаю, как это совместить с async. Подскажите, пожалуйста, какой правильный способ отклонить промис в async функции? И как это правильно типизировать в 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.