Eduardo Eduardo - 4 months ago 8
Javascript Question

Avoiding javascript callback and promise hell

I have many asynchronous methods to execute and my program flows can change a lot depending on each method return. The logic below is one example. I could not write it in a easy-to-read way using Promises. How would you write it?

Ps: more complex flows are welcome.

Ps2: is_business is a predefined flag where we say whether we are writing a "business user" or a "person user".

begin transaction
update users
if updated
if is_business
update_business
if not updated
insert business
end if
else
delete business
end if
else
if upsert
insert user
if is_business
insert business
end if
end if
end if
commit transaction

Answer

The nice thing about promises is that they make a simple analogy between synchronous code and asynchronous code. To illustrate (using the Q library):

Synchronous:

var thisReturnsAValue = function() {
  var result = mySynchronousFunction();
  if(result) {
    return getOneValue();
  } else {
    return getAnotherValue();
  }
};

try {
  var value = thisReturnsAValue();
  console.log(value);
} catch(err) {
  console.error(err);
}

Asynchronous:

var Q = require('q');

var thisReturnsAPromiseForAValue = function() {
  return Q.Promise(function() {
    return myAsynchronousFunction().then(function(result) {
      if(result) {
        // Even getOneValue() would work here, because a non-promise
        // value is automatically cast to a pre-resolved promise
        return getOneValueAsynchronously();
      } else {
        return getAnotherValueAsynchronously();
      }
    });
  });
};

thisReturnsAPromiseForAValue().then(function(value) {
  console.log(value);
}, function(err) {
  console.error(err);
});

You just need to get used to the idea that return values are always accessed as arguments to then-callbacks, and that chaining promises equates to composing function calls (f(g(h(x)))) or otherwise executing functions in sequence (var x2 = h(x); var x3 = g(x2);). That's essentially it! Things get a little tricky when you introduce branches, but you can figure out what to do from these first principles. Because then-callbacks accept promises as return values, you can mutate a value you got asynchronously by returning another promise for an asynchronous operation which resolves to a new value based on the old one, and the parent promise will not resolve until the new one resolves! And, of course, you can return these promises from within if-else branches.

The other really nice thing illustrated in the example above is that promises (at least ones that are compliant with Promises/A+) handle exceptions in an equally analogous way. The first error "raised" bypasses the non-error callbacks and bubbles up to the first available error callback, much like a try-catch block.

For what it's worth, I think trying to mimic this behavior using hand-crafted Node.js-style callbacks and the async library is its own special kind of hell :).

Following these guidelines your code would become (assuming all functions are async and return promises):

beginTransaction().then(function() {
  // beginTransaction() has run
  return updateUsers(); // resolves the boolean value `updated`
}).then(function(updated) {
  // updateUsers() has "returned" `updated`
  if(updated) {
    if(isBusiness) {
      return updateBusiness().then(function(updated) {
        if(!updated) {
          return insertBusiness();
        }
        // It's okay if we don't return anything -- it will
        // result in a promise which immediately resolves to
        // `undefined`, which is a no-op, just like a missing
        // else-branch
      });
    } else {
      return deleteBusiness();
    }
  } else {
    if(upsert) {
      return insertUser().then(function() {
        if(isBusiness) {
          return insertBusiness();
        }
      });
    }
  }
}).then(function() {
  return commitTransaction();
}).then(function() {
  console.log('all done!');
}, function(err) {
  console.error(err);
});