ChanandlerBong ChanandlerBong - 3 months ago 5
Javascript Question

Promises: Execute something regardless of resolve/reject?

Using the Promises design pattern, is it possible to implement the following:

var a, promise

if promise.resolve
a = promise.responsevalue;

if promise.reject
a = "failed"

AFTER resolution/rejection. Not ASYNC!!
send a somewhere, but not asynchronously. //Not a promise


What I'm looking for is something like
finally
in a
try - catch
situation.

PS: I'm using the ES6 Promise polyfill on NodeJS

Answer

If you return a value from catch, then you can just use then on the result of catch.

thePromise.then(result => doSomething(result)
          .catch(error => handleErrorAndReturnSomething(error))
          .then(resultOrReturnFromCatch => /* ... */);

...but that means you're converting a rejection into a resolution (by returning something from catch rather than throwing or returning a rejected promise), and relies on that fact.


If you want something that transparently passes along the resolution/rejection without modifying it, there's nothing built into ES6 promises that does that, but it's easy to write:

Object.defineProperty(Promise.prototype, "finally", {
    value(f) {
        return this.then(
            result => this.constructor.resolve(f()).then(() => result),
            error  => this.constructor.resolve(f()).then(() => { throw error; })
        );
    }
});

Example:

Object.defineProperty(Promise.prototype, "finally", {
  value(f) {
    return this.then(
      result => this.constructor.resolve(f()).then(() => result),
      error => this.constructor.resolve(f()).then(() => {
        throw error;
      })
    );
  }
});
test("p1", Promise.resolve("good")).finally(
  () => {
    test("p2", Promise.reject("bad"));
  }
);
function test(name, p) {
  return p.then(
    result => {
      console.log(name, "initial resolution:", result);
      return result;
    },
    error => {
      console.log(name, "initial rejection; propagating it");
      throw error;
    }
  )
  .finally(() => {
    console.log(name, "in finally");
  })
  .then(
    result => {
      console.log(name, "resolved:", result);
    },
    error => {
      console.log(name, "rejected:", error);
    }
  );
}

A couple of notes on that:

  1. Note the use of this.constructor so that we're calling resolve on whatever kind of promise (including a possible subclass) created the original promise; this is consistent with how Promise.resolve and others work, and is an important part of supporting subclassed promises.

  2. The above is intentionally not including any argument to the finally callback, and no indication of whether the promise was resolved or rejected, in order to be consistent with finally in the classic try-catch-finally structure. But if one wanted, one could easily pass some of that information into the callback.

  3. The above does use the return value of the finally callback, which is probably not consistent with #2 above. I'll have to think about that...


Or if you prefer to subclass Promise rather than modifying its prototype:

class PromiseX extends Promise {
    finally(f) {
        return this.then(
            result => this.constructor.resolve(f()).then(() => result),
            error  => this.constructor.resolve(f()).then(() => { throw error; })
        );
    }
}
PromiseX.resolve = Promise.resolve;
PromiseX.reject = Promise.reject;

Example:

class PromiseX extends Promise {
  finally(f) {
    return this.then(
      result => this.constructor.resolve(f()).then(() => result),
      error  => this.constructor.resolve(f()).then(() => { throw error; })
    );
  }
}
PromiseX.resolve = Promise.resolve;
PromiseX.reject = Promise.reject;

test("p1", PromiseX.resolve("good")).finally(
  () => {
    test("p2", PromiseX.reject("bad"));
  }
);
function test(name, p) {
  return p.then(
    result => {
      console.log(name, "initial resolution:", result);
      return result;
    },
    error => {
      console.log(name, "initial rejection; propagating it");
      throw error;
    }
  )
  .finally(() => {
    console.log(name, "in finally");
  })
  .then(
    result => {
      console.log(name, "resolved:", result);
    },
    error => {
      console.log(name, "rejected:", error);
    }
  );
}


You've said you want to do it without either extending the Promise.prototype or subclassing. You can give yourself a utility function, but it's more of a pain to call:

function always(f) {
  return [
    result => Promise.resolve(f()).then(() => result),
    error  => Promise.resolve(f()).then(() => { throw error; })
  ];
}

Usage:

thePromise.then(...always(/*..your function..*/)).

Note the use of the spread operator, so always can supply both arguments to then.

Example:

function always(f) {
  return [
    result => Promise.resolve(f()).then(() => result),
    error  => Promise.resolve(f()).then(() => { throw error; })
  ];
}

test("p1", Promise.resolve("good")).then(...always(
  () => {
    test("p2", Promise.reject("bad"));
  }
));
function test(name, p) {
  return p.then(
    result => {
      console.log(name, "initial resolution:", result);
      return result;
    },
    error => {
      console.log(name, "initial rejection; propagating it");
      throw error;
    }
  )
  .then(...always(() => {
    console.log(name, "in finally");
  }))
  .then(
    result => {
      console.log(name, "resolved:", result);
    },
    error => {
      console.log(name, "rejected:", error);
    }
  );
}


In the comments you expressed a concern that the finally wouldn't wait for the promise; here's that last always example again, with delays to demonstrate that it does:

function always(f) {
  return [
    result => Promise.resolve(f()).then(() => result),
    error  => Promise.resolve(f()).then(() => { throw error; })
  ];
}

test("p1", 500, false, "good").then(...always(
  () => {
    test("p2", 500, true, "bad");
  }
));

function test(name, delay, fail, value) {
  // Make our test promise
  let p = new Promise((resolve, reject) => {
    console.log(name, `created with ${delay}ms delay before settling`);
    setTimeout(() => {
      if (fail) {
        console.log(name, "rejecting");
        reject(value);
      } else {
        console.log(name, "resolving");
        resolve(value);
      }
    }, delay);
  });

  // Use it
  return p.then(
    result => {
      console.log(name, "initial resolution:", result);
      return result;
    },
    error => {
      console.log(name, "initial rejection; propagating it");
      throw error;
    }
  )
  .then(...always(() => {
    console.log(name, "in finally");
  }))
  .then(
    result => {
      console.log(name, "resolved:", result);
    },
    error => {
      console.log(name, "rejected:", error);
    }
  );
}


It occurs to me that you've said you're using a Promise polyfill and "would love" to use ES6, suggesting that you're not using it now. So all of the above with arrow functions and spread operators is of...limited utility. :-)

Here's the version extending Promise.prototype in ES5. I do think this is the simplest way to integrate this functionality into a Promise polyfill:

Object.defineProperty(Promise.prototype, "finally", {
    value: function(f) {
        var ctor = this.constructor;
        return this.then(
            function(result) {
                return ctor.resolve(f()).then(function() {
                    return result;
                })
            },
            function(error) {
                return ctor.resolve(f()).then(function() {
                    throw error;
                });
            }
          );
    }
});

Example:

Object.defineProperty(Promise.prototype, "finally", {
  value: function(f) {
    var ctor = this.constructor;
    return this.then(
      function(result) {
        return ctor.resolve(f()).then(function() {
          return result;
        })
      },
      function(error) {
        return ctor.resolve(f()).then(function() {
          throw error;
        });
      }
    );
  }
});

test("p1", Promise.resolve("good")).finally(function() {
  test("p2", Promise.reject("bad"));
});

function test(name, p) {
  return p.then(
      function(result) {
        console.log(name, "initial resolution:", result);
        return result;
      },
      function(error) {
        console.log(name, "initial rejection; propagating it");
        throw error;
      }
    )
    .finally(function() {
      console.log(name, "in finally");
    })
    .then(
      function(result) {
        console.log(name, "resolved:", result);
      },
      function(error) {
        console.log(name, "rejected:", error);
      }
    );
}

Comments