For example, regarding more graceful error handling, some API designs provide for split callbacks (one for the success notification, one for the error notification):
In APIs of this design, often the 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):
function response(err,data) {
// error?
if (err) {
console.error( err );
}
// otherwise, assume success
else {
console.log( data );
}
}
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.
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:
Here’s how you use it:
// using "error-first style" callback design
function foo(err,data) {
if (err) {
console.error( err );
else {
console.log( data );
}
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!” () and Isaac Z. Schlueter’s “Designing APIs for Asynchrony” (http://blog.izs.me/post/59142742143/designing-apis-for-asynchrony).
Consider:
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:
function asyncify(fn) {
var orig_fn = fn,
intv = setTimeout( function(){
intv = null;
if (fn) fn();
}, 0 )
;
fn = null;
// firing too quickly, before `intv` timer has fired to
// indicate async turn has passed?
fn = orig_fn.bind.apply(
orig_fn,
// add the wrapper's `this` to the `bind(..)`
// call parameters, as well as currying any
// passed in parameters
[this].concat( [].slice.call( arguments ) )
);
}
// already async
else {
// invoke original function
orig_fn.apply( this, arguments );
}
}
You use asyncify(..)
like this:
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 0
— result(..)
cannot help but be invoked asynchronously, which means the 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.