6

JavaScript Промисы - reject против throw

1

Я прочитал несколько статей на эту тему, но все еще не совсем понимаю, в чем заключается разница между использованием Promise.reject и выбрасыванием ошибки. Например,

Использование Promise.reject

return asyncIsPermitted()
    .then(function(result) {
        if (result === true) {
            return true;
        }
        else {
            return Promise.reject(new PermissionDenied());
        }
    });

Использование throw

return asyncIsPermitted()
    .then(function(result) {
        if (result === true) {
            return true;
        }
        else {
            throw new PermissionDenied();
        }
    });

Я предпочитаю использовать throw, так как это короче, но меня интересует, есть ли какие-либо преимущества одного подхода над другим.

5 ответ(ов)

5

Нет никаких преимуществ в использовании одного подхода по сравнению с другим, однако есть конкретный случай, когда throw не сработает. Тем не менее, эти случаи можно исправить.

В любой момент, когда вы находитесь внутри обратного вызова промиса, вы можете использовать throw. Тем не менее, если вы находитесь в любом другом асинхронном обратном вызове, вы должны использовать reject.

Например, следующий код не вызовет обработчик catch:

new Promise(function() {
  setTimeout(function() {
    throw 'или нет';
    // return Promise.reject('или нет'); тоже не сработает
  }, 1000);
}).catch(function(e) {
  console.log(e); // не произойдет
});

Вместо этого вы получите неопределенный промис и необработанное исключение. Это случай, когда вы предпочтете использовать reject. Однако вы можете исправить это двумя способами.

  1. Используя функцию reject оригинального промиса внутри таймаута:
new Promise(function(resolve, reject) {
  setTimeout(function() {
    reject('или нет');
  }, 1000);
}).catch(function(e) {
  console.log(e); // работает!
});
  1. Промиссифицировав таймаут:
function timeout(duration) { // Спасибо joews
  return new Promise(function(resolve) {
    setTimeout(resolve, duration);
  });
}

timeout(1000).then(function() {
  throw 'работает!';
  // return Promise.reject('работает'); тоже сработает
}).catch(function(e) {
  console.log(e); // 'работает!'
});
3

Важный момент заключается в том, что reject() НЕ завершает поток управления, как это делает оператор return. В отличие от этого, throw действительно завершает поток управления.

Вот пример:

new Promise((resolve, reject) => {
  throw "err";
  console.log("Никогда не достигнуто");
})
.then(() => console.log("Решено"))
.catch(() => console.log("Отклонено"));

в отличие от этого:

new Promise((resolve, reject) => {
  reject(); // аналогично работает и resolve()
  console.log("Всегда достигнуто"); // "Отклонено" будет выведено ПОСЛЕ этого
})
.then(() => console.log("Решено"))
.catch(() => console.log("Отклонено"));

В первом случае, при использовании throw, управление сразу переходит в блок catch, и строка "Никогда не достигнуто" не будет выполнена. Во втором случае, после вызова reject(), код продолжит выполняться, и "Всегда достигнуто" будет выведено в консоль перед тем, как сработает catch.

0

Да, самая большая разница заключается в том, что reject — это функция обратного вызова, которая выполняется после того, как промисс был отклонён, тогда как throw не может быть использован асинхронно. Если вы решите использовать reject, ваш код будет продолжать выполняться в асинхронном режиме, тогда как throw будет приоритизировать завершение функции разрешения (эта функция выполнится немедленно).

Пример, который помог мне понять эту тему, заключается в том, что вы можете установить функцию Timeout с reject, например:

new Promise((resolve, reject) => {
  setTimeout(() => { reject('err msg'); console.log('finished'); }, 1000);
  return resolve('ret val');
})
.then((o) => console.log("RESOLVED", o))
.catch((o) => console.log("REJECTED", o));

Такой код было бы невозможно написать с использованием throw:

try {
  new Promise((resolve, reject) => {
    setTimeout(() => { throw new Error('err msg'); }, 1000);
    return resolve('ret val');
  })
  .then((o) => console.log("RESOLVED", o))
  .catch((o) => console.log("REJECTED", o));
} catch (o) {
  console.log("IGNORED", o);
}

В маленьком примере автора вопроса разница может быть незаметна, но при работе с более сложными асинхронными концепциями разница между двумя подходами может быть весьма значительной.

0

Кратко: Функция трудно использовать, когда она иногда возвращает промис, а иногда выбрасывает исключение. При написании асинхронной функции лучше сигнализировать о неудаче, возвращая отклонённый промис.

Ваш конкретный пример затушёвывает некоторые важные различия между этими подходами.

Поскольку вы обрабатываете ошибки внутри цепочки промисов, выброшенные исключения автоматически превращаются в отклонённые промисы. Это может объяснить, почему они кажутся взаимозаменяемыми, но это не так.

Рассмотрим следующую ситуацию:

checkCredentials = () => {
    let idToken = localStorage.getItem('some token');
    if (idToken) {
        return fetch(`https://someValidateEndpoint`, {
            headers: {
                Authorization: `Bearer ${idToken}`
            }
        });
    } else {
        throw new Error('No Token Found In Local Storage');
    }
}

Это будет антипаттерном, поскольку вам придётся поддерживать как асинхронные, так и синхронные случаи ошибок. Это может выглядеть примерно так:

try {
    function onFulfilled() { /* ... остальная логика */ }
    function onRejected() { /* обработка асинхронной ошибки - например, тайм-аут сети */ }
    checkCredentials(x).then(onFulfilled, onRejected);
} catch (e) {
    // Error('No Token Found In Local Storage')
    // обработка синхронной ошибки
}

Это не совсем правильно, и именно здесь Promise.reject (доступный в глобальной области видимости) приходит на помощь и тем самым эффективно отличается от throw. Рефакторинг теперь выглядит так:

checkCredentials = () => {
    let idToken = localStorage.getItem('some_token');
    if (!idToken) {
        return Promise.reject('No Token Found In Local Storage');
    }
    return fetch(`https://someValidateEndpoint`, {
        headers: {
            Authorization: `Bearer ${idToken}`
        }
    });
}

Теперь вы можете использовать только один catch() для сетевых сбоев и для синхронной проверки отсутствия токена:

checkCredentials()
    .catch((error) => {
        if (error == 'No Token') {
            // показать модальное окно без токена
        } else if (error === 400) {
            // показать модальное окно "не авторизован" и т.д.
        }
    });
0

Вот пример, который вы можете попробовать. Просто измените isVersionThrow на false, чтобы вместо выброса исключения использовать отклонение промиса.

const isVersionThrow = true

class TestClass {
  async testFunction() {
    if (isVersionThrow) {
      console.log('Версия с выбрасыванием')
      throw new Error('Неудача!')
    } else {
      console.log('Версия с отклонением')
      return new Promise((resolve, reject) => {
        reject(new Error('Неудача!'))
      })
    }
  }
}

const test = async () => {
  const test = new TestClass()
  try {
    var response = await test.testFunction()
    return response 
  } catch (error) {
    console.log('ОШИБКА ВОЗВРАЩЕНА')
    throw error 
  }  
}

test()
.then(result => {
  console.log('результат: ' + result)
})
.catch(error => {
  console.log('ошибка: ' + error)
})

Попробуйте изменить значение isVersionThrow и посмотрите, как это повлияет на обработку ошибок. Если isVersionThrow равно true, будет выброшено исключение, а если false — промис будет отклонён.

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь