Do I need to return after early resolve/reject?

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:

  1. pending - initial state. From pending we can move to one of the other states
  2. fulfilled - successful operation
  3. 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.


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.


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.


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.