Unleashing Zalgo

Zalgo is Bad News.

This is in fact about conventions for using callbacks in Javascript.

Set expectations for APIs with callbacks

When you call a function in Javascript that takes a callback argument, you should know whether that callback will be called synchronously or asynchronously, i.e. on the next (or some future) event loop tick.

performAction(result => { console.log("received", result) })
console.log("after action");
Synchronously
Prints "received" and then "after action"
Asynchronously
Prints "after action" and then "received"

Opening a portal

The following code contains a way to unleash Zalgo:

const xhr = nanoajax.ajax(nanoOptions, (code, responseText) => {
	const contentType = xhr.getResponseHeader("Content-Type"); // ZALGO
	callback(code, contentType, responseText);
});

What exactly happens if the result callback is invoked synchronously?

Javascript variable definitions are block-scoped, so referring to xhr before it is initialised is permitted, event though it's a const

Avoiding the trap 1

If the callback may be deferred, always defer it. Maybe wrap it in a function that simply does a setTimeout(callback,0)

There is a dezalgo package for this.

Avoiding the trap 2

In fact, nanoajax provides a solution by passing the XHR as a third parameter to the callback:

const xhr = nanoajax.ajax(nanoOptions, (code, responseText, xhr) => {
	const contentType = xhr.getResponseHeader("Content-Type");
	callback(code, contentType, responseText);
});

This shadows the xhr declaration, so it has its own ugliness, but does work.

Avoiding the trap 3

Use Promises: they will always defer execution of subsequent steps

const promise = new Promise(resolve => {
	resolve("xyzzy");
}).then(x => console.log("result", x));
console.log("promise", promise);
// Prints:
//   "promise" ...
//   "result"

But note that the executor is invoked before the Promise constructor returns!

This creates a fun alternative way to defer execution:

Promise.resolve(true).then(() => {
	console.log("deferred");
});

This also explains why Sinon's clock fakery doesn't work with code that uses Promises, JSYK

Avoiding the trap 4

zen-observable goes to some effort to avoid unleashing Zalgo:

const Observable = require("zen-observable");
const subscription = new Observable(observer => {
	observer.next("abcde");
	observer.next("xyzzy");
	observer.complete();
}).subscribe(value => {
	console.log("received", value);
	subscription.unsubscribe();
});
// prints only "abcde"