Do I need to return after early resolve/reject?
JavascriptPromiseEs6 PromiseJavascript Problem Overview
Suppose I have the following code.
function divide(numerator, denominator) {
return new Promise((resolve, reject) => {
if(denominator === 0){
reject("Cannot divide by 0");
return; //superfluous?
}
resolve(numerator / denominator);
});
}
If my aim is to use reject
to exit early, should I get into the habit of return
ing immediately afterward as well?
Javascript Solutions
Solution 1 - Javascript
The return
purpose is to terminate the execution of the function after the rejection, and prevent the execution of the code after it.
function divide(numerator, denominator) {
return new Promise((resolve, reject) => {
if (denominator === 0) {
reject("Cannot divide by 0");
return; // The function execution ends here
}
resolve(numerator / denominator);
});
}
In this case it prevents the resolve(numerator / denominator);
from executing, which is not strictly needed. However, it's still preferable to terminate the execution to prevent a possible trap in the future. In addition, it's a good practice to prevent running code needlessly.
Background
A promise can be in one of 3 states:
- pending - initial state. From pending we can move to one of the other states
- fulfilled - successful operation
- rejected - failed operation
When a promise is fulfilled or rejected, it will stay in this state indefinitely (settled). So, rejecting a fulfilled promise or fulfilling a rejected promise, will have not effect.
This example snippet shows that although the promise was fulfilled after being rejected, it stayed rejected.
function divide(numerator, denominator) {
return new Promise((resolve, reject) => {
if (denominator === 0) {
reject("Cannot divide by 0");
}
resolve(numerator / denominator);
});
}
divide(5,0)
.then((result) => console.log('result: ', result))
.catch((error) => console.log('error: ', error));
So why do we need to return?
Although we can't change a settled promise state, rejecting or resolving won't stop the execution of the rest of the function. The function may contain code that will create confusing results. For example:
function divide(numerator, denominator) {
return new Promise((resolve, reject) => {
if (denominator === 0) {
reject("Cannot divide by 0");
}
console.log('operation succeeded');
resolve(numerator / denominator);
});
}
divide(5, 0)
.then((result) => console.log('result: ', result))
.catch((error) => console.log('error: ', error));
Even if the function doesn't contain such code right now, this creates a possible future trap. A future refactor might ignore the fact that the code is still executed after the promise is rejected, and will be hard to debug.
Stopping the execution after resolve/reject:
This is standard JS control flow stuff.
- Return after the
resolve
/reject
:
function divide(numerator, denominator) {
return new Promise((resolve, reject) => {
if (denominator === 0) {
reject("Cannot divide by 0");
return;
}
console.log('operation succeeded');
resolve(numerator / denominator);
});
}
divide(5, 0)
.then((result) => console.log('result: ', result))
.catch((error) => console.log('error: ', error));
- Return with the
resolve
/reject
- since the return value of the callback is ignored, we can save a line by returning the reject/resolve statement:
function divide(numerator, denominator) {
return new Promise((resolve, reject) => {
if (denominator === 0) {
return reject("Cannot divide by 0");
}
console.log('operation succeeded');
resolve(numerator / denominator);
});
}
divide(5, 0)
.then((result) => console.log('result: ', result))
.catch((error) => console.log('error: ', error));
- Using an if/else block:
function divide(numerator, denominator) {
return new Promise((resolve, reject) => {
if (denominator === 0) {
reject("Cannot divide by 0");
} else {
console.log('operation succeeded');
resolve(numerator / denominator);
}
});
}
divide(5, 0)
.then((result) => console.log('result: ', result))
.catch((error) => console.log('error: ', error));
I prefer to use one of the return
options as the code is flatter.
Solution 2 - Javascript
A common idiom, which may or may not be your cup of tea, is to combine the return
with the reject
, to simultaneously reject the promise and exit from the function, so that the remainder of the function including the resolve
is not executed. If you like this style, it can make your code a bit more compact.
function divide(numerator, denominator) {
return new Promise((resolve, reject) => {
if (denominator === 0) return reject("Cannot divide by 0");
^^^^^^^^^^^^^^
resolve(numerator / denominator);
});
}
This works fine because the Promise constructor does nothing with any return value, and in any case resolve
and reject
return nothing.
The same idiom can be used with the callback style shown in another answer:
function divide(nom, denom, cb){
if(denom === 0) return cb(Error("Cannot divide by zero"));
^^^^^^^^^
cb(null, nom / denom);
}
Again, this works fine because the person calling divide
does not expect it to return anything and does not do anything with the return value.
Solution 3 - Javascript
If you don't "return" after a resolve/reject, bad things (like a page redirect) can happen after you meant for it to stop. Source: I ran into this.
Solution 4 - Javascript
Technically it is not needed here1 - because a Promise can be resolved or rejected, exclusively and only once. The first Promise outcome wins and every subsequent result is ignored. This is different from Node-style callbacks.
That being said it is good clean practice to ensure that exactly one is called, when practical, and indeed in this case since there is no further async/deferred processing. The decision to "return early" is no different than ending any function when its work is complete - vs. continuing unrelated or unnecessary processing.
Returning at the appropriate time (or otherwise using conditionals to avoid executing the "other" case) reduces the chance of accidentally running code in an invalid state or performing unwanted side-effects; and as such it makes code less prone to 'breaking unexpectedly'.
1 This technically answer also hinges on the fact that in this case the code after the "return", should it be omitted, will not result in a side-effect. JavaScript will happily divide by zero and return either +Infinity/-Infinity or NaN.
Solution 5 - Javascript
The answer by Ori already explains that it is not necessary to return
but it is good practice. Note that the promise constructor is throw safe so it will ignore thrown exceptions passed later in the path, essentially you have side effects you can't easily observe.
Note that return
ing early is also very common in callbacks:
function divide(nom, denom, cb){
if(denom === 0){
cb(Error("Cannot divide by zero");
return; // unlike with promises, missing the return here is a mistake
}
cb(null, nom / denom); // this will divide by zero. Since it's a callback.
}
So, while it is good practice in promises it is required with callbacks. Some notes about your code:
- Your use case is hypothetical, don't actually use promises with synchronous actions.
- The promise constructor ignores return values. Some libraries will warn if you return a non-undefined value to warn you against the mistake of returning there. Most aren't that clever.
- The promise constructor is throw safe, it will convert exceptions to rejections but as others have pointed out - a promise resolves once.
Solution 6 - Javascript
In many cases it is possible to validate parameters separately and immediately return a rejected promise with Promise.reject(reason).
function divide2(numerator, denominator) {
if (denominator === 0) {
return Promise.reject("Cannot divide by 0");
}
return new Promise((resolve, reject) => {
resolve(numerator / denominator);
});
}
divide2(4, 0).then((result) => console.log(result), (error) => console.log(error));