Skip to content

Make always() more sane #103

@briancavalier

Description

@briancavalier

Right now, promise.always() is weird for a few reasons, several of which were discussed in this google group thread. For instance:

  1. It receives either a fulfilled value, or a rejection reason and can't easily distinguish, except by inspecting the value/reason itself. This makes the function passed to always() trickier to implement and potentially more error prone.
  2. It is easy to accidentally "handle" a rejection simply by returning. For example, passing the identity function or a function that returns undefined, like console.log to always() will always squelch errors.

One idea would be to try to make always() work more like synchronous finally. Consider the following code:

try {
  return doSomething(x);
} catch(e) {
    return handleError(e);
} finally {
    cleanup();
}

Notice that this finally clause does not have access to the return value unless we do some extra work above and capture it in a variable. So, the "default" is for finally NOT to have access to the return value or to the exception. While cleanup can throw an exception, thus transforming an originally-successful return into a failure, it cannot turn a failure into a successful return by not throwing.

Now consider how the current always() implementation works in this code:

var fn = require('when/function');
fn.call(doSomething, x)
  .otherwise(handleError)
  .always(cleanup);

There are two obvious differences:

  1. cleanup has access to the result or the error.
  2. cleanup can modify the outcome of the operation in a way that it can't above: it can transform a failure into success simply by returning (even if it simply returns its input, i.e. the identity function!).

Here is a proposed version of always that I think behaves more like (but still not exactly like) finally:

Promise.prototype.always = function(callback) {
  return this.then(
    function(value) {
      return when(callback(), function() {
        return value;
      });
    },
    function(error) {
      return when(callback(), function() {
        return when.reject(error)
      });
    });
  )
}

Notice that callback is not given access to either the fulfillment value or the rejection reason. It can still turn a success into a failure by throwing, but it cannot turn a failure into a success simply by returning. It also cannot change the ultimate fulfillment value.

I'm becoming a fan of this, but want to see what other folks think.

One potential variant of this would be to pass the value/reason to callback, but still to disallow the case of turning a failure into success. That would look like:

Promise.prototype.always = function(callback) {
  return this.then(
    function(value) {
      return when(callback(value), function() {
        return value;
      });
    },
    function(error) {
      return when(callback(error), function() {
        return when.reject(error)
      });
    });
  )
}

Thoughts? Alternatives?

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions