Skip to content

Instantly share code, notes, and snippets.

@jish
Created October 28, 2014 22:35
Show Gist options
  • Save jish/e9bcd75e391a2b21206b to your computer and use it in GitHub Desktop.
Save jish/e9bcd75e391a2b21206b to your computer and use it in GitHub Desktop.
An example "always" behavior for ES6 promises. This only works if you do not create / return intermediate promises.
// A thing I want to do
// This flow only involves **one** promise, for example an ajax call
// None of the subsequent `then` or `catch` calls, return new promises.
var explode = false;
var promise = new Promise(function(resolve, reject) {
if (explode) {
reject();
} else {
resolve();
}
}).then(function() {
console.log('Thing is done. Do followup task.');
return 'my argument';
}).then(function(arg1) {
console.log('Thing is done. Do followup task 2. argument: ' + arg1);
}).catch(function() {
console.log('Thing failed');
}).then(function() {
console.log('Always do this');
});
// => "Thing is done. Do followup task."
// => "Thing is done. Do followup task 2. argument: my argument"
// => "Always do this"
// Set explode to true
var explode = true;
var promise = new Promise(function(resolve, reject) {
if (explode) {
reject();
} else {
resolve();
}
}).then(function() {
console.log('Thing is done. Do followup task.');
return 'my argument';
}).then(function(arg1) {
console.log('Thing is done. Do followup task 2. argument: ' + arg1);
}).catch(function() {
console.log('Thing failed');
}).then(function() {
console.log('Always do this');
});
// => "Thing failed"
// => "Always do this"
@danielstjules
Copy link

Thanks for taking the time!

@mischkl
Copy link

mischkl commented Feb 25, 2015

This behavior was for me admittedly somewhat unexpected. I had assumed that a rejection anywhere in the chain stays rejected permanently, so in case of rejection the last then() would never get called. But it turns out that by calling catch() the rejection is considered to have been "handled", so a resolved promise is returned. Thus any then() that is chained after a catch() will always be called.

Actually this makes sense when one thinks of it as being equivalent to synchronous code - everything that comes after a catch block is always executed. Thus as seen in this gist "always" can be simulated via an empty catch followed by a then.

Note that unlike the jQuery Deferred .always(), any additional catch() calls chained after the last then() will not be executed. In this way the gist code is semantically more like "finally" than "always", since it simultaneously ends the error-handling block. If subsequent catch calls should continue to work it would be necessary to throw an error in the above catch function.

In the end, both versions - finally and always - can easily be added to the Promise prototype as follows:

Promise.prototype.finally = function(onResolveOrReject) {
  return this.catch(function(reason){
    return reason;
  }).then(onResolveOrReject);
};
Promise.prototype.always = function(onResolveOrReject) {
  return this.then(onResolveOrReject,
    function(reason) {
      onResolveOrReject(reason);
      throw reason;
    });
};

@Download
Copy link

The trick is to think of it in terms of try...catch. So this code:

someActionThatMightFail()
.then(..)
.catch(..)

is effectively equivalent to:

try {
  someActionThatMightFail()
  // code here is equivalent to `then`
}
catch() {
  // code here is equivalent to `catch`
}

Now, when you chain promises after the catch handler, like this:

someActionThatMightFail()
.then(..)
.catch(..)
.then(someOtherRiskyAction)
.catch(..)

you are effectively creating this pattern:

try {
  try {
    someActionThatMightFail()
    // code here is equivalent to the first `then`
  }
  catch() {
    // code here is equivalent to the first `catch`
  }

  // code here is equivalent to the second `then`
}
catch() {
  // code here is equivalent to the second `catch`
}

I find that when I think of it this way, it makes a lot more sense.

One other thing to keep in mind:

no return statement == return undefined == return Promise.resolve()

In other words, if your catch handler does not return, it implicitly returns undefined, which is automatically converted to Promise.resolve(). To reject, you should either return Promise.reject() or throw new Error(..).

@henkin
Copy link

henkin commented Oct 12, 2016

Thanks, y'all. That clarified some things for me.

@pennyandsean
Copy link

Thanks for this. Nice and clear.

@ArndBrugman
Copy link

Top! was looking for exactly this transition away from
.always
your to
.then(function(){}).catch(function(){}).then
is a great solution for me. Thx!

@SDemonUA
Copy link

SDemonUA commented May 8, 2018

@ArndBrugman
You can avoid .catch Identifier by using:

.then(function(){}, function(){}).then(function(){ /* this will be always */ });

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment