Do I need to return after early resolve/reject?

sam picture sam · Sep 12, 2015 · Viewed 87.3k times · Source

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 returning immediately afterward as well?

Answer

Ori Drori picture Ori Drori · Sep 12, 2015

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.