Wednesday, September 6, 2017

Promises, promises: a case for synchronous execution

On occasion I find myself counted as a resource for both front-end development and server-side development, particularly when the server-side development is in nodejs. I suppose that’s one of the advantages of having written in JavaScript since before it was JavaScript. One of the difficulties I’ve found in that endeavor, however, is when engineers who are less experienced with JavaScript start using Google to answer their questions and those answers duplicate some very poor habits. This quickly becomes the case when one of those poor habits - the overuse, or inappropriate use, of asynchronous functions like Promises, for example - has become very popular.

Not wishing to be misunderstood, I should perhaps rewind a bit and explain. In front-end development, we learned very early that JavaScript is single-threaded, and as such, would block processes. As front-end engineers concerned about performance, this was drilled into our heads at every opportunity. This was especially true for those of us writing interfaces where performance was more than just a ‘nice to have’ feature - interfaces where delays of a few seconds in a process would reduce revenue by tens of millions of dollars.

In those days, we did everything we knew to reduce blocking processes and even developed interfaces that integrated custom events and event handlers (before we actually called it the pub-sub model). When server-side JavaScript came along - for real this time- it made sense to a lot of people to use the same sort of models used in developing for user-agents because by that time we'd had years of to learn about JavaScript. What did all of our experience with JavaScript in a user-agent teach us? All of our experience taught us two things that stood out above all others: JavaScript is single-threaded and blocking processes is not nice. The folks who wrote about nodejs even wrote extensively about how the functions you write shouldn’t block processes and the ‘best practices’ discouraged use of the *sync methods - like readFileSync or writeFileSync - in the nodejs core modules. All this came about because (say it with me) JavaScript is single-threaded and that was a significant problem in code running in a user-agent.

There is, however, a dirty little secret that few in this new world want to acknowledge - some processes should be blocked.

We’ve heard it said that if you have a hammer everything looks like a nail, and that's certainly been true in this instance. However, coding a server-side process is categorically different than coding for a user-agent. In a user-agent, blocking a process impedes a person's action - action that is often randomly ordered rather than sequential. Blocking action increases the cognitive load of the process, which in turn increases the amount of time required to complete the process the user is trying to complete, and longer processes cost more. As an aside here, I should point out that even in user-agent we recognize that there are times we should or even must block processes.

So, how does this affect current practices that encompass server-side development? The focus on asynchronous processes - a large part of front-end development for very good reasons - has led to the proliferation of Promise objects. On the server side, there has been a corresponding decrease in the implementation of synchronous methods alongside it.

At this point you may be asking why, or even if, that's bad, because JavaScript is single-threaded and blocking processes is not nice, and the short answer is "yes". This quickly becomes clear if we consider a simple case in which process logging and auditing becomes critical - a process for which we must have those services in place before continuing or exiting the process. There are still deeper reasons, however.


The answer is also yes because - and I know this will be a stretch for some readers - there are a lot of differences between server-side processes and user-agent processes. As a clue to what some of those differences might be, one of the descriptions includes the word user. Even if we set aside how a user injects asynchronous behavior into a process and how that alters how we think about designing a process, there are still sufficient answers to why the proliferation of Promises in server-side development is disadvantageous.


Good code is well written...and, I've said this before (and even written it) but it bears repeating - good writing has clarity. Writing what should be a synchronous process as a then clause on a Promise masks that the process is synchronous. That masking of reality is not only not clear, it is the opposite of clear. While one might posit that writing can be good even if it lacks clarity, it certainly is not good when its true nature is cloaked in obscurity. Additionally, even though a "Promise chain" with a series of then clauses may be organized, it certainly does not have greater clarity than code that is explicit about its true, synchronous nature.

Good code is as simple as possible. We must, for a number of reasons, consider complexity the antithesis of good code - not the least of these reasons is that complex systems fail in complex ways. Coding a synchronous process as a Promise chain introduces unnecessary complexity that goes beyond a lack of clarity. Further, when Engineer Adam writes a process as a Promise and Engineer Beatrice has to use that in a synchronous process she will have to introduce await (as a module dependency until it becomes part of the native implementation), further increasing the complexity.

While the journey is interesting - as the "how did we get here" questions often are - we must move beyond that as the question in the mind of senior engineers becomes "how do we best address this situation".

The first step in addressing any problem is admitting there is one. We, collectively, must recognize that just as there are legitimate uses of asynchronous processes, there are legitimate uses of asynchronous processes. As long as we insist on the vilification of a synchronous process, we will never move forward.

Admitting there is a problem is merely the first step, however. Beyond that we, as engineers, must commit to building interfaces that allow synchronous use as well as asynchronous use. We must also commit to evaluating the execution needs of the application we are building to determine the appropriate course of action, and then following that course of action even in the face of the hue and cry of those who would insist we follow an asynchronous path.

As stated earlier, there are times in which asynchronous execution is the appropriate solution, but we should not adhere to that simply because nodejs evangelists or anyone else says we should.

Happy coding.


No comments:

Post a Comment