The First Thing to Remember When Working With Promises

Written by Yuri Takhteyev

Working with JavaScript requires mastering asynchronous code. The two most
common patterns for doing so are callbacks and promises. Promises are great
because they offer us more flexibility: if a function gets a promise from
another function it can either attach callbacks to it or just pass the
callback to the caller.

Therein, however, lies the danger of forgetting to do either, which can lead
to hard-to-track bugs. For that reason, the #1 thing to remember when working
with JavaScript promises is:

When you get a promise from another function, ALWAYS attach an error handler at the end of the chain, UNLESS you are going to return the final promise to the caller.

Angular 2 Custom Training

If you do not, then any errors in the code are going to be silently swallowed,
which makes debugging a nightmare. The only time you can skip an error
handler is when you are passing the promise (and the responsibility) to the
caller.

Let’s look at a few examples based on a hypothetical getPromise() function
that returns a promise, and a synchronous process() function that does
something with the result of the promise:

  getPromise()
    .then(function(result) {
      process(result);
    });

This is REALLY BAD. Any errors either in getPromise() or in process() would disappear into thin air. Basically, what this code says is: “If something goes wrong, don’t bother me, because I really don’t care.”

So, let’s attach an error handler:

getPromise()
  .then(function(result) {
    process(result);
  }, $log.error);

Here we are passing Angular’s $log.error() error logging function as the error handler. This is an improvement, but still rather poor. In fact, this code can be even more dangerous, since it may provide a false sense of security. In this sample, errors in getPromise() will get logged but errors in process() will still get swallowed. Why? Because we attached $log.error to the original promise only! Errors in process(), however, would lead to the rejection of the new promise returned by then(). That promise does not have any error handlers attached to it. And it should.

getPromise()
  .then(function(result) {
    return process(result);
  })
  .then(null, $log.error);

This is GOOD. All errors would be logged. Of course, simply logging an
error is rarely the best choice - you should probably do something to notify
the user. But even simply attaching $log.error as the error handler would make
a huge difference in the ease of debugging. Also, notice that you do not need
to attach an error handler to the earlier then’s, as long as you attach one at
the very end.

An aside on the then(null, errorHandler) pattern: Different promise
implementations provide for different functions for attaching the error
callback: .catch(), .fail(), onError(), etc. Nearly all of them,
however, allow you to attach an error handler with .then(null,
errorHandler).
Sticking with this pattern makes it easier to swap one library
for another, e.g., in the context of testing.

Now, what if you are want to return a promise? This changes things.

return getPromise()
  .then(function(result) {
    return process(result);
  });

This is OK. You are passing the responsibility for the error to your
caller. If anything goes wrong either in getPromise() or in process(), your
caller will get a rejected promise. This means, of course, that are trusting
your caller to do the right thing. Hopefully, your caller is careful enough
to handle the rejection. Sometimes it makes sense to catch some of the errors
yourself. Sometimes it is better to send them to the caller.

What if we just log the error ourselves:

return getPromise()
  .then(function(result) {
    return process(result);
  })
  .then(null, $log.error);

This may be ok, as long as you understand what you are doing. You are are
catching all the errors yourself and sending your caller a promise that
cleanly resolves to “undefined.” Your caller won’t know there was an error.
They may wonder why the result is “undefined.”

We do not have to choose between those to approaches, however. We can do both:

return getPromise()
  .then(function(result) {
    return process(result);
  })
  .then(null, function(error) {
    $log.error(error);
    throw error;
  });

Here we are logging the error and then re-throwing it. The caller will get a rejected promise, much as if we hadn’t attached any handlers to it, but the error will have been logged. This is the safest, most defensive approach. It may even be worth wrapping this handler in a named function to make it reusable:

function logAndRethrow(error) {
  $log.error(error);
  throw error;      
}

. . .

return getPromise()
  .then(function(result) {
    return process(result);
  })
  .then(null, logAndRethrow);

Happy hacking!