dean.edwards.name/weblog/2009/03/callbacks-vs-events/

Callbacks vs Events

Most of the major JavaScript libraries claim to support custom events in one form or another. For example, jQuery, YUI and Dojo all support a custom “document ready” event. However, the implementation of these custom events is always some form of callback system.

A callback system works by storing event handlers in an array. When the underlying event is detected the dispatch system loops through the array calling the callback functions in turn. So what’s wrong with that? Before I answer that, let’s look at some code.

Here is some simple code that uses the DOMContentLoaded event to perform two separate initialisations:

document.addEventListener("DOMContentLoaded", function() {
  console.log("Init: 1");
  DOES_NOT_EXIST++; // this will throw an error
}, false);

document.addEventListener("DOMContentLoaded", function() {
  console.log("Init: 2");
}, false);

What do you expect to see in the console when the document is loaded?

Well, you should see this (or something like it):

Init: 1

Error: DOES_NOT_EXIST is not defined

Init: 2

The point is, both functions are executed. You get an error in the first function but it does not stop the second function from executing.

The Problem

Now let’s look at some code based on a callback system. I’ll pick on jQuery because it’s the most popular:

$(document).ready(function() {
  console.log("Init: 1");
  DOES_NOT_EXIST++; // this will throw an error
});

$(document).ready(function() {
  console.log("Init: 2");
});

What do we see in the console?

Init: 1

Error: DOES_NOT_EXIST is not defined

The problem is clear. Callback systems are brittle. If any of the callback functions throw an error then the subsequent callbacks are not executed. In reality, this means that a poorly written plugin can prevent other plugins from initialising.

Dojo suffers exactly the same problem as jQuery. The YUI library takes a slightly different approach. It wraps a try/catch around its dispatch mechanism. The downside is that your errors occur silently:

YAHOO.util.Event.onDOMReady(function() {
  console.log("Init: 1");
  DOES_NOT_EXIST++; // this will throw an error
});

YAHOO.util.Event.onDOMReady(function() {
  console.log("Init: 2");
});

Produces:

Init: 1

Init: 2

Perfect initialisation! Nothing to worry about here! Except for the error that you don’t see.

So what’s the solution?

The Solution

The solution is to use a hybrid of a callback system and real event dispatch. We can trigger a fake event and from within that event, run the callback function. Each event handler has its own execution context. If an error occurs in our fake event then it won’t affect our callback system.

That sounds a bit complicated. I’ll illustrate with some code.

var currentHandler;

if (document.addEventListener) {
  document.addEventListener("fakeEvents", function() {
    // execute the callback
    currentHandler();
  }, false);

  var dispatchFakeEvent = function() {
    var fakeEvent = document.createEvent("UIEvents");
    fakeEvent.initEvent("fakeEvents", false, false);
    document.dispatchEvent(fakeEvent);
  };
} else { // MSIE

  // I'll show this code later
}

var onLoadHandlers = [];
function addOnLoad(handler) {
  onLoadHandlers.push(handler);
};

onload = function() {
  for (var i = 0; i < onLoadHandlers.length; i++) {
    currentHandler = onLoadHandlers[i];
    dispatchFakeEvent();
  }
};

Now we’ll use the code above to attach our two troublesome event handlers:

addOnLoad(function() {
  console.log("Init: 1");
  DOES_NOT_EXIST++; // this will throw an error
});

addOnLoad(function() {
  console.log("Init: 2");
});

OK. Let’s run the code above and see what we get:

Init: 1

Error: DOES_NOT_EXIST is not defined

Init: 2

Perfect! Just what we want. Both event handlers are executed and we also get a message telling us about the error in the first handler. Great!

But what about Internet Explorer I hear you ask (I have good hearing). MSIE does not support the standard event dispatch system. It has its own method; fireEvent but that only works with real events (e.g. click).

Rather than explain the solution in words, here is the code:

var currentHandler;

if (document.addEventListener) {

  // We've seen this code already

} else if (document.attachEvent) { // MSIE

  document.documentElement.fakeEvents = 0; // an expando property

  document.documentElement.attachEvent("onpropertychange", function(event) {
    if (event.propertyName == "fakeEvents") {
      // execute the callback
      currentHandler();
    }
  });

  dispatchFakeEvent = function(handler) {
    // fire the propertychange event
    document.documentElement.fakeEvents++;
  };
}

A similar approach except that we use the proprietary propertychange event as the trigger.

Summary

I’ve shown a very simple example of how to use the uderlying event system to fire custom events. Library authors should be capable of seeing how this can be extended to fully support cross-browser custom events.

Update

Some commenters have suggested using setTimeout. Here is my response to that:

For this particular example, a timer will work fine. This is just an example to illustrate the technique. The real usefulness of this is for other custom events. Most libraries implement custom events using a callback system. As I illustrated, callback systems are brittle. Dispatching events with timers will work to a degree but it is not how a real event system works. In a real system, events are dispatched sequentially. There are other concerns, like cancelling an event and stopping the event from bubbling. This would be impossible with timers.

The important thing is that I’ve demonstrated a technique for wrapping a callback system in a real event dispatch system. That gives you the ability to fire real custom events in MSIE. If you are building an event system based on event delegation then this technique may also be interesting to you.

Update 2

It seems that Prototype uses an almost identical trick to fire custom events in MSIE:

http://andrewdupont.net/2009/03/24/link-dean-edwards/

Comments (67)

Leave a comment

Great to see you blogging again Dean, a nice article on a subject I never completely understood. But time to ask the obvious to you as a library author: “When will you implement this in base2?”

  • Comment by: Marijn Huizendveld
  • Posted:

Marijn, I recently implemented this in base2 but I mostly developed it for JSB, a behaviors library I’m building on top of base2. I have a bunch of new code to release soon. You will see previews on the base2 mailing list within a week or two.

I intend to blog a lot more this year too.:-)

  • Comment by: -dean
  • Posted:

I experimented a little with the JSB implementation but I was wondering if you were going to make it base2 “native” code.. Anyway it looks really nice and I’m looking forward to more blogposts and new code releases:P

  • Comment by: Marijn Huizendveld
  • Posted:

Yep, I’m glad to see you blogging again as well! It never occurred to me that libraries implemented this so poorly, interesting to see your solution!:)

  • Comment by: Marc
  • Posted:

Why not dispatch the handlers with setTimeout? That would be much simpler… But you probably see something which I am missing;-)

@Marijn, you can consider it base2 native code. base2 uses the standard dispatchEvent mechanism for custom events. This technique is used to enable it for MSIE.

  • Comment by: -dean
  • Posted:

@Doeke, If I used setTimeout then the event is “dead” by the time the handler gets it. That means that you can’t cancel the event using preventDefault. Also, events occur sequentially. setTimeout would completely break that.

Although, for this example, setTimeout would work just as well.

If you want to fully support cross-browser custom events then you need a dispatch mechanism for MSIE. This is the only way I can think of dong it.

  • Comment by: -dean
  • Posted:

@Doeke Zanstra:
+1

I was just about to mention this. In my library projects which utilize user-supplied callbacks, I use timeout(func, 0). For example:

try {
    callback();
}
catch(e){
    setTimeout(function(){
        throw e;
    }, 0);
}

This prevents the execution from aborting because of a bad callback, and it is cross-browser. Is there something less favorable about this approach? The callbacks are occurring sequentially, but the errors are reported to the browser asynchronously.

[…] Dean Edwards explains how the standard “callback” pattern in JavaScript is too brittle for something like a “DOM ready” event. Prototype appears to be the one major library that handles this “correctly.” WIN! […]

Weston, you posted your comment as I was responding to Doeke. For this particular example, a timer will work fine. This is just an example to illustrate the technique. The real usefulness of this is for other custom events. Most libraries implement custom events using a callback system. As I illustrated, callback systems are brittle. Dispatching events with timers will work to a degree but it is not how a real event system works. In a real system, events are dispatched sequentially. There are other concerns, like cancelling an event and stopping the event from bubbling. This would be impossible with timers.

  • Comment by: -dean
  • Posted:

Whoa, you’re blogging again!

  • Comment by: Ilya
  • Posted:

@Dean: I forgot to mention this, but I’m glad you started blogging again. I had seen the event dispatching code (especially the onpropertychange), but didn’t quite grasp it. It becomes much clearer now; I’m learning so much again:-)

  • Comment by: Doeke Zanstra
  • Posted:

@Dean, fun writeup — I like the concept. So Dojo doesn’t have ‘custom events’ in this sense (though I’d like it to) but we have pub/sub, which as you are likely well aware is just function connections. I wrote a quick thing to make dojo.addOnLoad “behave” in this sense using the topic api already in place:

var d = dojo, oal = d.addOnLoad;
d.addOnLoad = function(fn){ // doesn't handle scope like addOnLoad does
	d.subscribe("/window/onload", function(){
		try{ fn(); }catch(e){ console.warn(e); }
	});
}
oal(d.partial(d.publish, "/window/onload")); 

… just thinking out loud. Nothing to see here.:)

Regards, Peter

@Peter, I always liked Dojo’s pub/sub functionality. And you’re just showing off with that partial application.;-)

  • Comment by: -dean
  • Posted:

@dean – I found a bug … I forgot to disconnect. We do the lazy-loading, so onLoad will fire every time. var s = dojo.subscribe(function(){ dojo.unsubscribe(s); }) …

and I wasn’t trying to show off anything but how fun JavaScript is with the partial call, I promise. What a wonderful language it is, eh? hitch/partial/curry/bind/whateveryoucallthatpattern should part of the spec.

Another approach is to buffer possible exceptions until all callbacks have been visited. Mochikit does this for example.

  • Comment by: Fredrik
  • Posted:

@Fredrik, I prefer my JavaScript to break where the actual error is. If you buffer exceptions then it is pretty hard to debug. How do you inspect the stack if the error has already passed?

  • Comment by: -dean
  • Posted:

@Peter: Function.prototype.bind is part of ES 3.1 and let’s you do partial application, so it looks like your wishes will be granted.

Dean, great and simple explanation on how to profit this useful behavior of events.

For me no doubt an event is generally better then a timer. The properties you may read out from the system objects in a timed event may be different from those read out during an event notification.

Now we have to learn how to profit the asynchronous aspects of it and avoid its pitfalls too. I imagine we can now have a better use of “throw”…

Have you thought about the necessity to handle failures of previous callbacks from successive callbacks (plugins depending on others) ?

Have you thought about the necessity to handle failures of previous callbacks from successive callbacks (plugins depending on others) ?

I can’t say that I have.:-)I’m trying to solve one problem here. Giving a separate execution context to each event handler. That’s it.

  • Comment by: -dean
  • Posted:

FYI, I’ve never used this code, but here’s Mochikit’s callback routine. Apparently, 1 error = it’s rethrown. > 1 error = A new error thrown with an “errors” property containing all collected errors.

  • Comment by: Steve Clay
  • Posted:

Dojo unifies events into after-advice on function calls. We treat DOM events this way and we treat regular function application just the same. It’s all unified by dojo.connect(), therefore the idiomatic way to do this in Dojo is:

// listen on the dojo.loaded() function
dojo.connect(dojo, "loaded", function(){ ... });

Regards

hmm.. very clever:)

I believe and I think it’s worth noting too.. that collecting callbacks via native addEventListener and then firing them using dispatchEvent is more efficient than collecting it to JavaScript array and iterating over it. However I haven’t tested it.

I wonder if it’s possible to make some use of third argument (capture/bubble phase) in custom events.. will it accept just true/false or maybe wider set of possibilities.

  • Comment by: medikoo
  • Posted:

@dean, comment form doesn’t accept ‘+’ in email address.. is it bug or a feature ?

  • Comment by: medikoo
  • Posted:

Rather than have a global currentHandler variable, it’s a better idea to pass the callback as a parameter to the function you’re invoking. This allows you to do things like arbitrarily nest sequences without conflicts.

I did a write-up on a technique I developed for this a year or so ago. You can find it at, http://blog.mendix.com/?p=4 .

Basically I use a sequencer function that passes a reference to an anonymous inner function to a function I shift off the array (which is a function that accepts a callback as its only parameter and invokes it on completion).

  • Comment by: Michiel Kalkman
  • Posted:

@Michiel, yes this isn’t the best way to write the code but it’s the easiest to understand. I always write my example code in the simplest way possible. A good programmer can always recode it to their tastes.

  • Comment by: -dean
  • Posted:

Good post. The other reason I like using events over call backs at times is for unit testability. I then find that with jQuery plugin code for example, unit testing them helps drive out the events that should be fired, creating a richer plugin as a result. Probably easiest way to explain is to point to a post about that I wrote a little while back: http://www.onenaught.com/posts/85/turn-your-jquery-code-into-a-richer-unit-testable-plugin

  • Comment by: Anup
  • Posted:

I’m actually knee-deep in this issue for a soon-to-be-released framework-agnostic unit test library I’m working on. As I’m trying to support as many platforms as possible, using custom events really wasn’t a option. Turns out some platforms also don’t have setTimeout (Opera mini, for example), so I ended up using recursion and a try..catch..finally block:

function iterate(callbacks, length, i) {
  if (i >= length) { return; }
  try {
    callbacks[i]();
  } catch(e) {
    throw e;
  } finally {
    iterate(callbacks, length, i+1);
  }
}

// clone callbacks array in case a callback is
// removed during iteration
var c = Array.prototype.slice.call(callbacks, 0);

iterate(c, c.length, 0);

In firebug:

var a = [
  function() { console.log(0); },
  function() { console.log(1); throw new Error; },
  function() { console.log(2); },
  function() { console.log(3); }
];

iterate(a, a.length, 0);
// -> 0
// -> 1
// -> 2
// -> 3
// -> Error

Any know issues with this solution ?

Hi Dean, nice to see you blogging again (did anybody said it?) I like the idea but I am not sure about one thing, the onpropertychange in the main DOM element. In FireFox we tested (me and other guys) an incremental and generic performances slowdown using DOMAttrModified event in the main node. I wonder if your choice could affect IE performances somehow, even if the callback is pretty simple. In few words, do you really need real the main node to fire these events? Why don’t you create just a single one “out of the box” to perform the same operation? Something like:

var manager = document.createElement("manager");
manager.fireEvent = function(){
    if(!this.parentNode)
        document.body.appendChild(this);
    ++this.property;
};
manager.onpropertychange = function(){
    alert("Hello!");
};
manager.style.display = "none";
manager.property = 0;

onload = function(){
    manager.fireEvent();
};

Which I guess suites perfectly with a DOMContentLoaded IE simulation since the body in that case should be always there.

Well… that’s what happens when you work too late. The above obviously only throws the first error… and stops further processing. Please just ignore.

[…] Dean Edwards: Callbacks vs Events I’ve shown a very simple example of how to use the uderlying event system to fire custom events. Library authors should be capable of seeing how this can be extended to fully support cross-browser custom events. (tags: javascript events callbakcs howto webdev) […]

Dean Edwards: Callbacks vs Events… Thank you for submitting this cool story – Trackback from DotNetShoutout…

[…] Dean Edwards talking about Callbacks vs. Events […]

[…] Callbacks vs Events касается обработки событий в jQuery […]

Good to see you today Dean! I was excited when I saw your name pop up in Google Reader, and you didn’t disappoint.

Good solution. I have used the try/catch approach recently in one of my projects and collect all errors in an Array along with the function name in which they occured. It’s really only a small init file, so it works fine. Will consider your approach on one of the next refactoring sessions.

  • Comment by: G
  • Posted:

[…] Edwards has a subtle piece on callbacks vs. events where he calls out the issue of libraries that support custom events dying out when one custom […]

[…] http://deanedwards.me.uk/weblog/2009/03/callbacks-vs-events/ : un article de Dean Edwards sur la différence entre callbacks et events […]

[…] Dean Edwards: Callbacks vs Events (tags: javascript) […]

@Andrea, I’m sorry that your comment did not appear immediately, it got trapped in my spam filter for some reason.

base2 does not actually use the documentElement for this. Instead, I create a <meta> element and attach the onpropertychange handler to that.

  • Comment by: -dean
  • Posted:

Hi Dean; it’s indeed great to see you blogging again; and a very interesting solution you came up with.

Just wondering if you had any chance to do some profiling on your code though, since (at least for IE) it seems like it touches the DOM. If it does, it might be significantly slower than a pure-JS solution…

For the record, YUI keeps a “lastError” property in YAHOO.util.CustomEvent with the latest silenced error, so you can still get to it, albeit I do agree it would be best to get “JavaScript to break where the actual error is”.

And Tobie’s solution does seem very interesting… have to go and try it (no pun intended).

@Nelson, whoah! Seeing your name takes me back.:-)

Please note folks, that the code I posted was an example. If you want to implement such a system then you will have to test and benchmark it yourself. For the record, in base2 I create a <meta> element to trap these changes as described in my comment above.

  • Comment by: -dean
  • Posted:

Better (but slower) custom events… Hurray, Dean Edwards (of IE7 fame) is blogging again. And he has an interesting take on Javascript custom events…His solution is indeed very good for the fact that it doesn’t resort to any pushing/shoving of exceptions to be able to use those except…

Dean, it has indeed been a while!

I’ve put some quick profiling together and it does seem like your solution is slower than pure JS, unfortunately…

http://fittopage.org/blog/17-Better-but-slower-custom-events

Then again I might have botched the comparison…

Yes, it’s slower. But how many listeners are you dispatching to? Ten or twenty at most, but more likely one, perhaps two. Sometimes people measure the wrong things. It’s what happens inside an event listener that’s important. The dispatch time is practically irrelevant.

  • Comment by: -dean
  • Posted:

@dean: that is true for the majority of situations, granted. Still, I have a couple of applications where the difference of hitting the DOM would be significant.

Not trying to come up with negative criticism, by the way!:)I do find it a really good solution. I’ll still try and profile Tobie’s solution…

@Nelson, do you have an example where you are dispatching lots of events at the same time? I’m interested in all use cases.

  • Comment by: -dean
  • Posted:

Hey I like the implementation for MSIE:)

Though, even if that “DOMContentLoaded” events got more cross-browser-compatible and precise. I still like that idea:

<script>main()</script>
</body>

And main() does all the rest. Well I know, this doesn’t work for all that little snipplets doing some weird mostly helpful:Dstuff, I want to throw that option in. Shame on me, if sb posted that one, I didnt read all the coments… *g*

-gossi

  • Comment by: gossi
  • Posted:

@Dean, I don’t really have a practical example to hand… just think complex financial application with lots of streaming prices (via Comet) triggering events to multiple components, at the rate of ~10/second.

The sort of thing that your grandma said should never be done with HTML and Javascript…:)

~10/second is not a lot.:-)

I really don’t think that this form of dispatch system is likely to be a bottle-neck.

  • Comment by: -dean
  • Posted:

@tobie langel – I rewamped your code, and it works fine now.
@dean – I chose this approach. As I tested, it’s roughly 50x faster. Do you see any problems?

function log(value) {
    document.body.innerHTML += value;
};

function iterate(array) {
    if (array.length) {
        try { array.shift()(); }
        finally { iterate(array); }
    }
};

iterate([
    function() { log(0); },
    function() { log(1); throw new Error; },
    function() { log(2); },
    function() { log(3); throw new Error; }
]);
  • Comment by: Daniel Steigerwald
  • Posted:

Even better solution (I hope). No recursion.

/*
* Safe event dispatching
*   - should not fail if handler causes an exception
*   - should not fail if some listener will remove itself or will add another listener
* Copyright (c) 2009 Daniel Steigerwald (http://daniel.steigerwald.cz), Mit Style License
*/

function dispatchEvents(array) { 
    array = Array.prototype.slice.call(array); // clone
    while(array.length) {
        try { array.shift()(); }
        finally { continue; }   
    }
};

dispatchEvents([
    function() { log(0); },
    function() { log(1); throw new Error; },
    function() { log(2); },
    function() { log(3); throw new Error; }
]);

Still, I see one small issue. Only IE is really throwing exceptions. Firefox is suspiciously silent, any idea?.

  • Comment by: Daniel Steigerwald
  • Posted:

thanks ^^ http://plugins.jquery.com/project/thread

  • Comment by: tenshi
  • Posted:

[…] preventDefault or stopPropagation or even an error prevents any additional handlers/listeners on the same element. So if you have two handlers […]

Hi Dean. Another excellent topic. This is not a problem of the callback system. This is problem of the libraries :)) My solution to throwing error with setTimeout.

try {
	handlers[i]();
}
catch (e)
{
	setTimeout(function(){throw e}, 0);
}
  • Comment by: SmashThePain
  • Posted:

@Weston Ruter sorry, i don’t look your post.

Dean, this is not use setTimeout(callback, 0); This is only for throwing error. This technique share to implement prevent event and cancelable.

  • Comment by: SmashThePain
  • Posted:

@SmashThePain: did you test your code?

Dean’s approach works well, but I afraid of performance when such dispatching is used on mousemove event for instance. Try/Finally works much better, except error stack is removed since finally has to be called. This makes debugging a bit harder.

@Daniel Steigerwald: yes i do, understand the problem of my code. Debugging is hard:( I think, that’s enough:

catch(e) {
	if (console) console.error(e);
	throw e;
}
finally {
	continue;
}

The problem here is, if you don’t have console object. But browsers like Chrome haves console in default mode. I hope in future all browsers haves console object.

Dean sorry for my flood, and my dummy english. With respect!

  • Comment by: SmashThePain
  • Posted:

HI Dean,

I am new to this callback implementation of javascript. After reading your blog, I got it for firefox. But for IE 7, onpropertchange event never get fire, so I am not able to run your code in IE 7.

  • Comment by: @newLearner
  • Posted:

[…] you don’t want that to block execution of the other callbacks or of the rest of your method. Dean Edwards wrote about this last year, though his post specifically deals with using the DOM to dispatch events since it […]

Another drawback of using setTimeout() is that it affords the browser to do something in between the callback executions. For example, if you’re implementing a graphically intense application, you may not want to incur redraws and reflows between each callback running.

In other words, setTimeout() is a performance killer.

I released a project that uses this technique to solve problems with async data sources and race conditions. Since IE still doesn’t support custom events, my library doesn’t support it, but you can find the other test casts and which data sources are supported on my project page: http://code.google.com/p/proto-q/

[…] […]

Great work, Dean. The perfect way to maintain DOM semantics when implementing an observer. If performance is a concern then you can register each callback as a listener directly. This jsperf tests show it is significantly faster than dispatching a new event for each one. Obviously try/catch is going to be faster than either of these…

Also, there’s a bug in firefox where no exception is sent to the console when using dispatchEvent. http://jsperf.com/multiple-listeners-vs-multiple-dispatches

  • Comment by: Will Alexander
  • Posted:

Hey Dean,

First off thanks for this write up, this really helped me a lot. I’m having some trouble with this in MSIE browsers. Instead of changing the property of ‘document.documentElement’, I’m changing the property of an ‘object’ element and I can’t seem to get the listener to fire when the property is changed.

I noticed that the one is of type ‘DispHTMLHtmlElement’ and the other is ‘DispHTMLObjectElement’. Is there any difference in the two?

Thanks in advance, Jason

  • Comment by: Jason Wong
  • Posted:

This fires an event when any properties are changed tho… no good.

  • Comment by: Edward
  • Posted:

It seems that MS has crippled the technique you have pointed out in IE.10 by removing support for propertychange detection on custom properties. A workaround is to use:

document.documentElement.setAttribute('customEventTrigger', newVal);

The custom event must be attached using:

document.documentElement.attachEvent('onpropertychange', myHandler);

I dont know if this will work in all older IE’s (?) and it will NOT work in IE 10:

attachEvent is no longer supported. Starting with Internet Explorer 11 Preview, use addEventListener (MSDN attachEvent)

It would have been nice to know if your technique modified to use setAttribute works in all browsers before IE 11

Will it be possible to register ‘onpropertychange’ using addEventListener in IE 11?

Only time will tell..

  • Comment by: Terje Rosenlund
  • Posted:

Comments are closed.