Why Are Exceptions Used for Rejecting Promises in Js

Why are exceptions used for rejecting promises in JS?

Why wasn't the api designed in such a fashion where for the then function, it is passed a resolve and reject function like the original promise constructor?

Actually, the API in that spec emerged as a consensus amongst various implementations. However, some points that might have led to this are:

  • then is a rather functional method. It's callback only should receive one data argument, the result value of the promise.
  • Passing additional resolve/reject functions to the callback does not work well with multiple arguments or even variadic functions.
  • then is usually used as a plain mapping function. You simply return the new value, no resolve is needed.
  • When you really want to do something asynchronous in your callback where you could make use of resolve/reject, you better should use a promise anyway - which you simply can return then.

I once implemented a Promise lib with optional resolve/reject arguments, but it was tedious to use - and I seldom needed them because of #4. Using them was error-prone, you could easily forget something (like handling errors, or progress events) - just like the people who are manually constructing and returning deferreds that are resolved from promise callbacks, instead of calling then.

Exceptions are heavy so it seems odd they are using them as a choice for flow control.

They're not really meant to be used for control flow (like branching, loops etc) but for exception handling: rejections are exceptional. Most Promise developers wanted to implement them as an alternative for synchronous (blocking) code - where IO was always throwing exceptions, so they adapted this. Rejections are still explained as the asynchronous equivalent to try … catch, though their monadic nature could be utilized in mightier ways and higher-level applications.

Creating a whole new promise object and returning it, just to reject it, adds to code bloat IMO.

There's not much difference between return new RejectedPromise(…), return reject(…) and throw Error(…).

Debugging becomes harder too in case of an exception is thrown (such as syntax errors, or if function is being called on a undefined object, etc.)

Most Promise developers seem to see this as an advantage actually - (unexpected) exceptions even in asynchronous code will be caught automatically, so they can be handled instead of blowing up the program (unnoticed). See also exception handling, thrown errors, within promises and acceptable promise pattern for 'LOUD' errors?.

JavaScript Promises - reject vs. throw

There is no advantage of using one vs the other, but, there is a specific case where throw won't work. However, those cases can be fixed.

Any time you are inside of a promise callback, you can use throw. However, if you're in any other asynchronous callback, you must use reject.

For example, this won't trigger the catch:

new Promise(function() {
setTimeout(function() {
throw 'or nah';
// return Promise.reject('or nah'); also won't work
}, 1000);
}).catch(function(e) {
console.log(e); // doesn't happen
});

Avoid uncaught exception when a promise gets rejected due to timeout?

Instead of racing a promise with a short-lived promise(rejectAfterDelay), we can wrap the promise in a short-lived promise:

async function delayedPromise(delay) {
return new Promise((res) => setTimeout(res, delay, 'success'));
}

// wrap the promise instead of racing it
function rejectAfterDelay(promise, ms) {
return new Promise((resolve, reject) => {
setTimeout(reject, ms, 'timeout');
// forward the reasons to the wrapper
promise.then(reason => resolve(reason))
.catch(err => reject(err));
});
}

async function main() {

// Create array of promises.
promArr = [];
promArr.push(delayedPromise(100));
promArr.push(delayedPromise(200));
promArr.push(delayedPromise(300));
promArr.push(delayedPromise(400));
promArr.push(delayedPromise(500));

// Wait for all promises to either get fulfilled or get rejected after 200 ms.
const msMaxTime = 200;
const result = await Promise.allSettled(
promArr.map(promise => {
//return Promise.race([promise, rejectAfterDelay(msMaxTime)]);
return rejectAfterDelay(promise, msMaxTime);
})
);

console.log(result.map(r => r.value ? r.value : r.reason));
}
main()

Difference of exception and unfulfilled promise?

An unfulfilled Promise is simply one that hasn't been fulfilled, which is quite possible even if it doesn't reject. For example:

const prom = new Promise((resolve) => {
// code here that never calls `resolve`
});

That is a Promise that will never be fulfilled - but it won't reject either.

An "unfulfilled" Promise may not necessarily hang forever like the above - it's simply a Promise that hasn't resolved yet.

a throw is always an exception?

To an extent, yes, though it behaves somewhat differently when inside a Promise - it won't cause an error event. Instead, it'll try to find a .catch handler in the Promise chain it's currently in, and if it doesn't find one, an unhandledrejection event will be fired.

And why are exceptions only for synchronous code?

That's just how the language was designed.

Why doesn't the try catch block catch the promise exception?

I can see two possible aspects to your question:

  1. The error being thrown in test is in the synchronous (not asynchronous) part of test. Why is it a promise rejection rather than a synchronous exception?

  2. The promise from test() is being rejected, why doesn't catch catch that rejection?

#1 - Why is it a rejection?

Because even if an async function throws during the synchronous portion of its work, that just rejects the promise it returns, it doesn't raise a synchronous error. It's just a design decision made during the design of async functions, and a smart one — if it were a synchronous error when throwing in the synchronous part of the function but a promise rejection after that, it would be chaotic and hard to understand. So it's simple: throwing in an async function always rejects its promise.

(This is consistent with how the promise constructor treats the executor callback you pass into it. When you do new Promise((resolve, reject) => /*...*/}), the promise constructor calls your function synchronously, but if you throw during the call, it uses what you throw to reject the promise rather than allowing it to continue as a synchronous exception.)

#2 - Why isn't rejection caught by the catch block?

Because you're not using await. Promise rejections are only exceptions when you are awaiting the promise. If you use the promise methods then/catch/finally to attach handlers, rejection is handled by calling the rejection handler, not by the exception mechanism.

So either use promise methods to attach fulfillment and rejection handlers:

test()
.then(result => {
// ...use `result` here...
})
.catch(error => {
// ...handle/report error here...
});

Or use await in an async function (or at the top level of a module if you have top-level await in your enviroment):

// In an `async` function (or the top level of a module in cutting-edge environments)
try {
const result = await test();
// ...use `result` here...
}
catch(err){
// ...handle/report error here...
}

Why does Promise.all() throw an exception even if I .catch() it?

As pointed out by Bergi in the comments...

If you execute your first bit of code without the catch statement (or actually print the errors you catch) you'll see what is happening.

Promise.all(new Promise((res, rej) => rej('Failure!')))

Returns:

Promise {<rejected>: TypeError}
Uncaught (in promise) Failure!
Uncaught (in promise) TypeError: object is not iterable (cannot read property Symbol(Symbol.iterator))
at Function.all (<anonymous>)
at <anonymous>:1:9

Notice the first error is the error we throw by rejecting the promise.

The second error comes from not using Promise.all() correctly, which is the error you were catching.

The error we throw by rejecting the promise is never caught because of the incorrect usage of the Promise.all() method.


Now, let's test out the code with our promise inside an array, which is the correct usage of Promise.all(), as pointed out by Barmar in the comments.

Promise.all([new Promise((res, rej) => rej('Failure!'))])
.catch(() => console.log("It's all okay."))

Returns:

Promise {<fulfilled>}
It´s all okay.

So, we are successfully catching the rejected promise error.

Also worth noting, the final promise returned is the promise that the catch() method returns after execution. Which is fulfilled since we successfully executed the catch statement, although the promise that Promise.all() actually returns is rejected.



Related Topics



Leave a reply



Submit