Avatar ·

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

Всем привет! Я недавно начал осваивать async/await в TypeScript и столкнулся с проблемой. У меня есть функция, которая должна выполнять асинхронную операцию, но при определённых условиях мне нужно вернуть ошибку (reject). Проблема в том, что я не понимаю, как правильно "отклонить" промис, когда использую синтаксис async/await. Вот пример моего кода: ```typescript async function fetchUserData(userId: string): Promise { // Хочу отклонить промис, если userId пустой if (!userId) { // Как здесь правильно сделать reject? } const response = await fetch(`/api/users/${userId}`); return response.json(); } ``` Я знаю, что в классическом Promise можно вызвать reject(), но в async функции return автоматически завершает промис с resolve. Я пробовал выбрасывать ошибку через throw, но это как-то коряво. Есть ли более правильный способ? Или throw — это и есть стандартный способ? Также интересует, как правильно типизировать такие ошибки, чтобы TypeScript не ругался. Заранее спасибо за помощь! <<>>

Правильный способ: используйте throw

Да, вы абсолютно правы — в функциях async стандартным и правильным способом отклонить промис является оператор throw. Это не "коряво", а наоборот — это идиоматичный подход, который делает код более читаемым и предсказуемым.

Когда вы используете throw внутри async функции, JavaScript автоматически оборачивает выброшенное значение в отклонённый промис (rejected promise). Это работает точно так же, как вызов reject() в конструкторе new Promise().

Вот как должен выглядеть ваш исправленный код:

async function fetchUserData(userId: string): Promise<UserData> {
  if (!userId) {
    throw new Error('userId не может быть пустым');
  }
  
  const response = await fetch(`/api/users/${userId}`);
  return response.json();
}

// Использование
try {
  const data = await fetchUserData('');
} catch (error) {
  console.error('Ошибка:', error.message); // "userId не может быть пустым"
}

Типизация ошибок в TypeScript

Чтобы сделать код ещё более надёжным, вы можете создать собственный класс ошибки:

class ValidationError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'ValidationError';
  }
}

async function fetchUserData(userId: string): Promise<UserData> {
  if (!userId) {
    throw new ValidationError('userId не может быть пустым');
  }
  
  const response = await fetch(`/api/users/${userId}`);
  
  if (!response.ok) {
    throw new Error(`HTTP ошибка: ${response.status}`);
  }
  
  return response.json();
}

Важные моменты:

  • throw внутри async функции — это единственный способ отклонить промис, кроме возврата другого отклонённого промиса
  • return Promise.reject() тоже работает, но это менее идиоматично:
async function fetchUserData(userId: string): Promise<UserData> {
  if (!userId) {
    return Promise.reject(new Error('userId не может быть пустым'));
  }
  // ... остальной код
}
  • Всегда используйте try/catch при вызове async функций, чтобы обработать возможные ошибки
  • TypeScript не может статически проверить, какие именно ошибки могут быть выброшены, поэтому хорошей практикой является создание пользовательских классов ошибок

Сравнение с классическим Promise

Для наглядности, вот как выглядит тот же код без async/await:

function fetchUserData(userId: string): Promise<UserData> {
  return new Promise((resolve, reject) => {
    if (!userId) {
      reject(new Error('userId не может быть пустым'));
      return;
    }
    
    fetch(`/api/users/${userId}`)
      .then(response => response.json())
      .then(resolve)
      .catch(reject);
  });
}

Как видите, синтаксис async/await значительно упрощает код, а throw заменяет reject() естественным образом.

И последнее: не забывайте, что если вы не обработаете ошибку через try/catch, она "всплывёт" до глобального обработчика, что может привести к неожиданным падениям приложения. Всегда обрабатывайте ошибки!

<<>>

Альтернативный подход: возврат объекта Result

В некоторых проектах (особенно в функциональном стиле) предпочитают не использовать исключения для управления потоком, а возвращать специальный объект, содержащий либо успешный результат, либо ошибку. Это называется паттерном Result или Either.

type Result<T> = 
  | { success: true; data: T }
  | { success: false; error: string };

async function fetchUserData(userId: string): Promise<Result<UserData>> {
  if (!userId) {
    return { success: false, error: 'userId не может быть пустым' };
  }
  
  try {
    const response = await fetch(`/api/users/${userId}`);
    const data = await response.json();
    return { success: true, data };
  } catch (error) {
    return { success: false, error: 'Ошибка загрузки данных' };
  }
}

// Использование
const result = await fetchUserData('123');
if (result.success) {
  console.log(result.data);
} else {
  console.error(result.error);
}

Преимущества:

  • Явная обработка ошибок — TypeScript заставляет проверять поле success
  • Нет неожиданных исключений
  • Удобно для композиции функций

Недостатки:

  • Больше шаблонного кода
  • Не все библиотеки поддерживают такой подход
  • Теряется возможность использовать try/catch для перехвата любых ошибок

Выбирайте подход в зависимости от стиля вашего проекта. Для большинства случаев стандартный throw — это оптимальное решение.