JavaScript: Callbacks, EventEmitters, and Promises – which one to use?!?

Short version

If you have something that’s simple and always synchronous, don’t use any of them. Just write a dumb function.

If you have a function that’s simple and only needs one asynchronous response – and there are no other potential responses – then a callback is fine.

If you have some kind of object that could have several different potential asynchronous responses – at various different points in lifecycle – and you might or might not want to listen to none, one, or more of them? Then use EventEmitters.

And finally, use Promises when:

  • You have a collection of asynchronous functions, and you need to respond only when all of them have returned, or any one of them have returned.
  • You’re doing mostly ‘imperative’ functions and don’t need to pass a lot of values around, you just need to chain together some callbacks in sequence.
  • You have some functions that might be synchronous, and some that might not be, and you’re not sure which until runtime
  • You have a collection of asynchronous events that are all firing, but the order that they must complete in is dependent upon some value determined only at runtime.
  • (weaker argument) You are falling victim to callback-hell, and your code is steadily creeping rightward

Long version

Functions


function foo(param1,param2,param3) {
  return "something";
}

//usage:
var a=foo(1,2,3);

If you can do it this way your life will be better. Do it this way if at all possible.

Simple Callbacks


function foo(param1,param2,param3,callback) {
  process.nextTick(function () {
    callback("something");
  });
}

//usage:
foo(1,2,3,function (result) {
  console.warn("Yay, we got result! "+result);
});

If you find you’re passing “callback1, callback2, callback3” definitely don’t do this. But for small, simple asynchronous functions, with not much else going on, this is still fine. Still pretty easy to reason about. As functions grow larger, and nested callbacks grow deeper, it gets harder and harder to reason about, though. The invocations of your little function probably ought not to be more than just a few lines; if they are, you should consider the next option…

EventEmitters

I think 95% of the EventEmitters I create end up being ‘classes’ that extend the EventEmitter class, and I think that’s probably a good way to do it.


   /* ..... */
   this.emit("begin","something");
   /* ..... */
   this.emit("success","something");
   /* ...... */ 
   this.emit("failure","something");
   /* ...... */
   this.emit("complete","something",success === true);

What’s great about this model is that someone who’s consuming this object might only care about one particular event – in which case they can listen for just that one. I believe it’s ok, and actually good, to emit liberally, even events that are similar but not the same (in my example “success”/”failure” as well as “complete”).

Another nice side-effect is that all of your various listen events (.on(foo)) help document what the callback is actually for. E.g. –


on("complete",function (param) {
  /* see? Now we know this event handler fires when things are complete! */
});

If you’re not careful, you can absolutely slide into callback-hell here. But this is my personal favorite pattern to use. It’s pretty extensible.

Never do synchronous callbacks; ever. If you want to do something ‘immediate’ at least wrap it in a process.nextTick(function () {/* blah */}); block; that’ll effectively be immediate but allow for someone to use it in the way most EventEmitters are used.

Never throw errors; just emit “error” instead.

Promises

These are massively over-hyped as the solution to everything. While they are actually very, very cool; they definitely have some real drawbacks.

  • They can get hard to debug
  • They can be confusing
  • missing something like a return – which is super-easy to do – may just cause silent code malfunctioning instead of issuing any kind of error
  • Propagating data forward from previously-resolved promises into later promises looks and acts weird.
  • You lose a lot of the benefits of ‘closing-over’ variables

But, when used properly – they can turn something nasty like this:


foo.on("bar",function (baz) {
  bif.on("blorgh",function (bling) {
    bloop.on("gloob",function (fweep) {
      /* .... */
    });
  });
});

Into something much prettier like this:


foo.bar().then(function (baz) {
  return "thing";
}).then(function(bling) {
  return "other_thing";
}).then(function (fweep) {
  return "last_thing";
});

Which, especially if you end up with a super-long list, can be helpful.

You can also use .catch() to grab any error in your list of actions – and that can be enormously useful.

Also, if you have an array of promises, you can do something like –


Promise.all(my_array_of_promises).then(function (results) {
  /* do something */
});

Which can be very, very handy.

Where it starts to get ugly is, if in that example I gave above foo.bar().... – if you need to treat an error condition for each of those steps slightly differently. Now you can throw various .catch statements after each .then statement, but I can imagine trying to visually read through that as being a nightmare.

The other huge thing here is that some promises can be fully synchronous – e.g. Promise.resolve(7) – that’s a promise that will resolve to the number 7. And some promises (well, probably most of them) are asynchronous. This is great, and the ability to unify these two modes together can be very helpful.

So, absolutely use them when they make sense. But my current thinking (which might change) is that you should use the simplest asynchronous mechanism that expresses what you need, without adding complexity. Step up the complexities of your tech as you need to, but not before.

Use the right tool for the job.

Leave a Reply

Your email address will not be published. Required fields are marked *