parente.dev

Error Handlers in dojo.Deferred Chains

February 06, 2011

Starting in 1.5, Dojo's Deferred class exposes a then() method that accepts three arguments: a success handler for callback(), an error handler for errback, and a progress handler for progress() updates. One interesting feature of the dojo.Deferred implementation is the ability of a callback handler to return a new deferred which becomes the target of the next handler scheduled on the original deferred.

Consider these two snippets. The first shows chaining on a single deferred. The second shows chaining with a new deferred returned within the chain.

var d1 = functionReturningADeferred();
d1.then(function () {
  console.log("first success");
}).then(function () {
  // scheduled on the same deferred as the first
  // runs if the first handler completes without an exception
  console.log("second success");
});

var d2 = functionReturningADeferred();
d2.then(function () {
  console.log("first success");
  return anotherDeferredReturningFunction();
}).then(function () {
  // scheduled on deferred from anotherDeferredReturningFunction
  // runs when the second deferred's callback is invoked
  console.log("second success");
});

Try #1: Error handlers everywhere

I recently wrote some code in which my deferred callback handlers returned new deferreds for chaining, but where any deferred in the chain might have its errback() invoked. I wanted my handler chain to cease firing after the first occurrence of an error with one and only one handler invoked for that error. I naively started with the following recipe:

var d = functionReturningADeferred();
d.then(
  function () {
    console.log("first success");
    return anotherDeferredReturningFunction();
  },
  function () {
    console.log("first error");
  }
)
  .then(
    function () {
      console.log("second success");
      return yetAnotherDeferredReturningFunction();
    },
    function () {
      console.log("second error");
    }
  )
  .then(
    function () {
      console.log("third success");
    },
    function () {
      console.log("third error");
    }
  );

To my surprise, when the very first deferred's errback() was invoked, the console output declared:

  first error
  second success
  third success

Likewise, when the second deferred's errback was invoked, the console output was:

  first success
  second error
  third success

I quickly realized my mistake: my error handlers did not return new deferreds. Therefore, each additional then() call in my chain registered handlers to be invoked by the success or failure of the previous set in the chain.

Try #2: Error handlers throwing errors

I next tried the following approach:

var d = functionReturningADeferred();
d.then(
  function () {
    console.log("first success");
    return anotherDeferredReturningFunction();
  },
  function (err) {
    console.log("first error");
    throw err;
  }
)
  .then(
    function () {
      console.log("second success");
      return yetAnotherDeferredReturningFunction();
    },
    function (err) {
      console.log("second error");
      throw err;
    }
  )
  .then(
    function () {
      console.log("third success");
    },
    function (err) {
      console.log("third error");
      throw err;
    }
  );

This code showed improvement, but still did not have my desired behavior. An errback() on the initial deferred produced the following output:

  first error
  second error
  third error

An errback() on the second showed the following output:

  first success
  second error
  third error

After a little more thinking, I realized the same problem plaguing my first approach was in effect here. My error handlers were still not returning new deferreds. The chained handlers were still operating on the success or failure conditions of their predecessors.

Solution: One terminal error handler

Finally, I hit upon a pattern with the behavior I wanted:

var d = functionReturningADeferred();
d.then(function () {
  console.log("first success");
  return anotherDeferredReturningFunction();
})
  .then(function () {
    console.log("second success");
    return yetAnotherDeferredReturningFunction();
  })
  .then(
    function () {
      console.log("third success");
    },
    function (err) {
      console.log("any error");
    }
  );

A errback() on the first deferred now resulted in the following output:

  any error

An errback() on the second produced in the following:

  first success
  any error

Why does it work?

Any error early in the chain falls through the chain until it reaches an error handler. Placing the error handler at the end of the chain guarantees it and it alone will fire after any errback() in the chain.

Try it yourself

I created a easy-to-run jsFiddle that demonstrates the second and third patterns mentioned in this post. Let me know if you find any other interesting behaviors.

Another Read: handlerbag on GitHub »

I recently pushed for a small utility of mine called handlerbag to github. The code defines a tiny controller for managing a bag of Tornado web server handlers. The controller supports auth using Google OpenID, dynamic loading and unloading of handlers, and simple persistence of handler configuration using anydbm.