I was able to read your code without getting lost

How to Keep Your Code Tidy By Avoiding Nested Closures

JavaScript's pattern for clean callbacks using “bind”

by Joe Honton

In this episode Antoní shares a forgotten pattern for dealing with ECMAScript's "this".

Ivana just finished fixing an obscure bug that had been discovered in the rwserve library. And in record time too — just two hours from checkout to pull-request. She surprised herself. Nothing broke in any of the test cases, and code review passed without any comments.

Admittedly, she was a bit nervous about it when the Jira issue was assigned to her. The codebase was large, and there were lots of features in it that she had never used. Also, this would be her first experience actually working on the code.

She got started the same as always. Reproduce the bug . . . track down the module that was the probable source of it . . . set the debugger to break upon entry to the module . . . step through the code with the failing input . . . inspect the likely variables . . . watch for unexpected state changes. All standard troubleshooting steps.

And there it was. Within minutes she knew what the problem was, and in less than an hour had figured out how to correct it.

Test. Commit. Pull request. Code review. Ready for deployment.

As she was poking around, she noticed that almost every module had been authored by Antoní. And since she had previously found him to be approachable, she wandered over to his office to have a chat.

She tentatively began with, "So I just made my first commit to one of the rwserve security modules," then waited at the door to see if he was going to engage.

Antoní looked up, "Hey, Ivana. That's cool. I didn't know you were being assigned maintenance issues on that. Do you need my help on something?"

"Nope. I'm finished and it's being deployed on Thursday."

"Congrats!"

"Thanks. So I wanted to mention that I found it really easy to figure out what was going on. Your code is nothing like Devin's or Ken's. There's something about it that makes it clear what's going on. Not sure what it is, but I was able to read it without getting lost."

"Oh, it must be my Pulitzer Prize winning comments," he joked.

"Well, there is that," she played along, "But I guess I'm amazed at how you were able to construct such a large library without nested callbacks, and without ever getting confused over this. I hate this and try to avoid it because it seems to be the root cause of so much trouble for me."

bind(this)

"Oh, that" he replied, "I think you probably didn't catch on to the secret sauce. It's the way I use bind.

He walked over to the whiteboard, and wiped off the red and black scribbles that were on it. "Let me show you how the SessionMonitor module is set up. It doesn't really matter what it does, only how it does it."

"Here's what I'm talking about. The module defines a single class, and everything is initialized in its constructor, like this —"

constructor(session, sessionId) {
this.session = session;
this.sessionId = sessionId;
this.streamId = 0;
this.remoteAddress = null;

this.boundConnect = this.onConnect.bind(this);
this.boundStream = this.onStream.bind(this);
this.boundTimeout = this.onTimeout.bind(this);
this.boundUncaught = this.onUncaughtException.bind(this);

session.on('connect', this.boundConnect);
session.on('stream', this.boundStream);
session.on('timeout', this.boundTimeout);
session.on('uncaughtException', this.boundUncaught);
}

"The pattern is easy to spot. First, any function that will be triggered in a callback role is bound to the class instance. Then each of the listeners that will be capturing those callbacks are registered using those bound function names.

"In this way, when the callbacks occur, the bound function's this is always the SessionMonitor's class instance.

"Here's a snapshot of what the class methods look like. Not in their entirely, but enough to get the gist of it.

onConnect(serverHttp2Session, tlsSocket) {
this.remoteAddress = tlsSocket.remoteAddress;
serverHttp2Session.setTimeout(30*1000);
}

onStream(stream, incomingRequestHeaders) {
log.trace(`SID=${this.sessionId}`);
this.streamId++;
...
}

onTimeout() {
this.session.close();
}

onUncaughtException(error) {
log.trace(`RA=${this.remoteAddress}`);
log.trace(`SID=${this.sessionId}`);
log.trace(`STRM=${this.streamId}`);
log.trace(`ERR=${error.message}`);
}

"Take a look at onConnect() which is the first callback received. It saves the socket's remote address to the class instance before establishing the session timeout. Later on, any method that needs it can get access to that remote address.

"When onStream() is triggered, it can access the current session's sessionId, because it's a property of the class, and it was initialized in the constructor. Also it can access and increment the next available streamId, because that too was initialized in the constructor.

"Similarly, when onTimeout() is triggered, it can access the server session, and properly close it.

"And if anything crazy happens, the onUncaughtException() callback will have all of the instance properties readily available for diagnostic purposes."

"It's not a new pattern. It has been around for ages."

"I guess it's the lack of closures that makes it so readable," Ivana observed.

"Exactly," Antoní beamed, "Closures were somehow elevated to a canonical pattern in the Node.js community — but for no apparent reason. Consider this: closures broaden the scope of a function, allowing a function to access the variables of its containing scope. This goes counter to the policy we've all professed — that we should be localizing variables, not globalizing them.

It's the lack of closures that makes it so readable

"Localizing scope is the whole purpose behind things like strict, var, let, export and import. We want to avoid polluting the outer namespace, so that the local namespace doesn't have accidental collisions.

"When you add properties to a class, you explicitly tell the compiler that those properties can only be accessed with respect to the instantiated object. For methods of the class, that means, for example, something like this.remoteAddress. For functions outside the class, where sm is an instance of SessionMonitor, that means either sm.remoteAddress or sm[remoteAddress].

"Callbacks per se are nothing new. And the event-driven nature of both Node.js on the backend and DOM on the frontend, follow an architectural pattern that's been around for decades. The argument that JavaScript is single-threaded, and that that somehow makes this unique, is short-sighted.

"I remember when Windows was first introduced. At that time it was a single-threaded 16-bit graphical user interface running inside the DOS operating system. The fact that it used messages, meant that we had to figure out a way to structure our code to dispatch and receive those messages. The pattern I just showed you is based on the patterns that Microsoft developed years ago.

"DOS!?" Ivana was incredulous, "JavaScript wasn't even around back then!"

"Patterns, I said. Remember, patterns transcend languages. So it's irrelevant whether it's C or C++ or JavaScript"

Ivana almost blushed. She knew that. Shifting the conversation just a bit, she asked, "Why do I see the idiom var that = this in so many other codebases?"

That's a relic of a time before bind was added in ECMAScript 2015. When using a closure, it was a way to preserve the outer scope's reference to this so that the inner function could get to it. It was necessary because the inner function has its own this pointing to an entirely different thing. When the inner function needs to access a property on the outer scope (let's use remoteAddress again), it uses the syntax that.remoteAddress.

"So it's an anti-pattern if you're using bind(this)," Ivana concluded.

"Exactly," Antoní smiled — he was getting through.

"Nice," she mumbled, mostly to herself, thinking about how she could apply what he taught her to her own code.

Ivana wasn't entirely sure she would be able to remember everything, so she snapped a quick pic of the whiteboard as a reminder.


Antoní went back to his work on brotli compression. But his mind wandered. He had forgotten what it was like to be a new engineer when things were in flux. How were the new guys supposed to recognize the difference between a good pattern and an obsolete idiom. What's to keep them from simply mimicking what they've seen in other people's code? How are they supposed to know there's a cleaner way to do things?


No minifig characters were harmed in the production of this Tangled Web Services episode.

Follow the adventures of Antoní, Bjørne, Clarissa, Devin, Ernesto, Ivana, Ken and the gang as Tangled Web Services boldly goes where tech has gone before.

How to Keep Your Code Tidy By Avoiding Nested Closures — JavaScript's pattern for clean callbacks using “bind”

🔗 🔎