Trying to Save Callbacks

There are several variations of callback design that have attempted to address some (not all!) of the trust issues we’ve just looked at. It’s a valiant, but doomed, effort to save the callback pattern from imploding on itself.

For example, regarding more graceful error handling, some API designs provide for split callbacks (one for the success notification, one for the error notification):

  1. function success(data) {
  2. console.log( data );
  3. }
  4. function failure(err) {
  5. console.error( err );
  6. }
  7. ajax( "http://some.url.1", success, failure );

In APIs of this design, often the failure() error handler is optional, and if not provided it will be assumed you want the errors swallowed. Ugh.

Note: This split-callback design is what the ES6 Promise API uses. We’ll cover ES6 Promises in much more detail in the next chapter.

Another common callback pattern is called “error-first style” (sometimes called “Node style,” as it’s also the convention used across nearly all Node.js APIs), where the first argument of a single callback is reserved for an error object (if any). If success, this argument will be empty/falsy (and any subsequent arguments will be the success data), but if an error result is being signaled, the first argument is set/truthy (and usually nothing else is passed):

  1. function response(err,data) {
  2. // error?
  3. if (err) {
  4. console.error( err );
  5. }
  6. // otherwise, assume success
  7. else {
  8. console.log( data );
  9. }
  10. }
  11. ajax( "http://some.url.1", response );

In both of these cases, several things should be observed.

First, it has not really resolved the majority of trust issues like it may appear. There’s nothing about either callback that prevents or filters unwanted repeated invocations. Moreover, things are worse now, because you may get both success and error signals, or neither, and you still have to code around either of those conditions.

Also, don’t miss the fact that while it’s a standard pattern you can employ, it’s definitely more verbose and boilerplate-ish without much reuse, so you’re going to get weary of typing all that out for every single callback in your application.

What about the trust issue of never being called? If this is a concern (and it probably should be!), you likely will need to set up a timeout that cancels the event. You could make a utility (proof-of-concept only shown) to help you with that:

  1. function timeoutify(fn,delay) {
  2. var intv = setTimeout( function(){
  3. intv = null;
  4. fn( new Error( "Timeout!" ) );
  5. }, delay )
  6. ;
  7. return function() {
  8. // timeout hasn't happened yet?
  9. if (intv) {
  10. clearTimeout( intv );
  11. fn.apply( this, [ null ].concat( [].slice.call( arguments ) ) );
  12. }
  13. };
  14. }

Here’s how you use it:

  1. // using "error-first style" callback design
  2. function foo(err,data) {
  3. if (err) {
  4. console.error( err );
  5. }
  6. else {
  7. console.log( data );
  8. }
  9. }
  10. ajax( "http://some.url.1", timeoutify( foo, 500 ) );

Another trust issue is being called “too early.” In application-specific terms, this may actually involve being called before some critical task is complete. But more generally, the problem is evident in utilities that can either invoke the callback you provide now (synchronously), or later (asynchronously).

This nondeterminism around the sync-or-async behavior is almost always going to lead to very difficult to track down bugs. In some circles, the fictional insanity-inducing monster named Zalgo is used to describe the sync/async nightmares. “Don’t release Zalgo!” is a common cry, and it leads to very sound advice: always invoke callbacks asynchronously, even if that’s “right away” on the next turn of the event loop, so that all callbacks are predictably async.

Note: For more information on Zalgo, see Oren Golan’s “Don’t Release Zalgo!” (https://github.com/oren/oren.github.io/blob/master/posts/zalgo.md) and Isaac Z. Schlueter’s “Designing APIs for Asynchrony” (http://blog.izs.me/post/59142742143/designing-apis-for-asynchrony).

Consider:

  1. function result(data) {
  2. console.log( a );
  3. }
  4. var a = 0;
  5. ajax( "..pre-cached-url..", result );
  6. a++;

Will this code print 0 (sync callback invocation) or 1 (async callback invocation)? Depends… on the conditions.

You can see just how quickly the unpredictability of Zalgo can threaten any JS program. So the silly-sounding “never release Zalgo” is actually incredibly common and solid advice. Always be asyncing.

What if you don’t know whether the API in question will always execute async? You could invent a utility like this asyncify(..) proof-of-concept:

  1. function asyncify(fn) {
  2. var orig_fn = fn,
  3. intv = setTimeout( function(){
  4. intv = null;
  5. if (fn) fn();
  6. }, 0 )
  7. ;
  8. fn = null;
  9. return function() {
  10. // firing too quickly, before `intv` timer has fired to
  11. // indicate async turn has passed?
  12. if (intv) {
  13. fn = orig_fn.bind.apply(
  14. orig_fn,
  15. // add the wrapper's `this` to the `bind(..)`
  16. // call parameters, as well as currying any
  17. // passed in parameters
  18. [this].concat( [].slice.call( arguments ) )
  19. );
  20. }
  21. // already async
  22. else {
  23. // invoke original function
  24. orig_fn.apply( this, arguments );
  25. }
  26. };
  27. }

You use asyncify(..) like this:

  1. function result(data) {
  2. console.log( a );
  3. }
  4. var a = 0;
  5. ajax( "..pre-cached-url..", asyncify( result ) );
  6. a++;

Whether the Ajax request is in the cache and resolves to try to call the callback right away, or must be fetched over the wire and thus complete later asynchronously, this code will always output 1 instead of 0result(..) cannot help but be invoked asynchronously, which means the a++ has a chance to run before result(..) does.

Yay, another trust issued “solved”! But it’s inefficient, and yet again more bloated boilerplate to weigh your project down.

That’s just the story, over and over again, with callbacks. They can do pretty much anything you want, but you have to be willing to work hard to get it, and oftentimes this effort is much more than you can or should spend on such code reasoning.

You might find yourself wishing for built-in APIs or other language mechanics to address these issues. Finally ES6 has arrived on the scene with some great answers, so keep reading!