Three Levels of Callback Abstractions

Matthew Molnar
2 min readNov 5, 2018

--

I’m doing some animation work in canvas on a side project and using window.requestAnimationFrame(cb) to know when to draw to the canvas. If you’re not familiar with this method, it accepts a callback function (here, cb) to be called asynchronously when the animation frame is ready to be rendered to.

Having worked with Promises and async / await for many years now, I’m a bit tired of having to resort to typical callback methods for my asynchronous programming. In the 2018 world of JavaScript, what can be done?

First: No Abstraction

(function gameLoop(){
window.requestAnimationFrame(()=>{
canvasContext.putImageData(imageData, 0, 0);
gameLoop();
});
})();

While things could be broken out a bit, the above works, and is simple, but not necessarily straight forward. To be honest, it’s kind of a mess. We have a named function expression being recursively called (which is the only reason for naming function expressions) by it’s own nested callback arrow function and initially immediately invoked. Not very clear.

Possible Option: Promise

I’m only listing directly using promises because they’re technically an abstraction of asynchronous programming. It would make little sense to use them by themselves in this case as we’d still need to pass a callback to then() which doesn’t improve our callback depth at all.

Second: Async / Await

animationFrame = () => 
new Promise(resolve => window.requestAnimationFrame(resolve));
(async function gameLoop() {
while (true) {
await animationFrame();
context.putImageData(imageData, 0, 0);
}
})();

Is this better? Well, in the actual game loop, we technically removed a callback nesting. If our parent function is async, it looks a bit cleaner:

while (true) {
await animationFrame();
context.putImageData(imageData, 0, 0);
}

Holy Grail: Asynchronous Iteration

function* run(path) {
while (true){
yield (new Promise(r => window.requestAnimationFrame(r)));
}
}

Now, the above could be the most confusing for programmers not familiar with generators. However, look at the code we use to call this in the main game function:

for await (const frame of run()){
context.putImageData(imageData, 0, 0);
}

That’s it! Though it’s only one line shorter than the async / await method (and technically requires an extra loop), I think it’s the most succinct of all the options.

--

--