What comes after callbacks but before Promises

Arijit Bhattacharya
Trying to Manipulate the DOM
5 min readAug 9, 2017

Slowly but steadily, our community has adopted Promises as the new default for expressing asynchronous operations. Many of us dive head first into learning the Promise APIs without thinking much about the abstraction. And this lack of understanding makes it overwhelming whenever we try to express not so trivial async operations, which is quite often.

Here we are going to pull back the curtain and understand one of the underpinnings of Promises, the abstraction referred to commonly as placeholder object or eventual object or future value.

Why Asynchronicity in JavaScript

JS engines are embedded in single threaded environments like a browser window or a node.js server.

The task of painting the window in a browser and requesting information from the server takes place in a single thread. If the requesting information from the server was blocking, a user would not be able to scroll down until the request resolved to a response. And that would be a very unpleasant user experience.

A better user experience must provide the user with a perception of distinct events happening simultaneously.

And this perception of simultaneity makes asynchronicity imperative to JavaScript.

Asynchronicity can be best described by now and later. It sets up a task or puts something into motion now and then wait for it to finish, so we can react to it later.

Callback as an Abstraction

A callback sets up a task now like request data from a url and registers a handler which will be executed later on completion of the task like render the data to the DOM.

A callback is used as a pattern to express continuity between two events in a program seperated by arbitary length of time. And this typical way of using callback is referred as the continuation pattern.

Shortcomings of Continuation Pattern

Modern applications put significant bussiness logic inside the client. And it involves repeated async communication with the server.

These asynchronous events which are many in number, most times are dependent on each other. And the dependency can take many forms.

  • One cannot initiate an asynchronous operation without the result of another asynchronous operation. For example, a request for comments on a post cannot be made unless information about the post is received.
  • An operation can only be performed after all requests have been resolved. For example, students can be ranked by a criteria only after we recieve information for all of them.
  • One can initiate multiple async operations at the same time but have to resolve them in order. For example, one can request all posts in a list but individual post needs to be rendered in order, one after the other.

These demands from modern applications requires fine control over a sequence of events where each step is async. In continuation pattern, we wait for the events to resolve to data and then proceed to the next step of the sequence. This manual ordering of events, readiness and the recipe to proceed to the next step results in nested callbacks. And more the number of steps, more we have to manually build and track the flow of data.

A better abstraction for consumer of the async data will be a placeholder object — an eventual result of an asynchronous operation. The consumer which is outside the async transaction need not bother about the readiness of the value. It may be available immediately or after any arbitrary length of time.

Lets take a look at a simple example — get the first article for my blog.

var myFirstArticlePlaceholder = queryMyBlogForArticles(1)var renderArticle = function (article) {renderToDOM(article);};myFirstArticlePlaceholder(renderArticle);

This pattern differs from the continuation pattern in the following ways

  • The setup of the async event, ;i.e. requesting the first article from the blog is completely decoupled from what we do with it.
  • This decoupling allows us to express the intent to consume the data whenever we want — renderArticle can be passed before or after myFirstArticlePlaceholder actually resolves to a value. This way we are abstracting time.
  • The placeholder here, myFirstArticlePlaceholder is just a simple function, so we can pass around the app and consume the data any number of times by just passing it a callback

This pattern of placeholder object is realised by wrapper over a state — myFirstArticlePlaceholder will act as a wrapper over the content of the first article. We can engage with the content whenever we want by passing it a callback.

Wrapper Over a State

The placeholder object is mysterious. It may have a value now or it may have a value later. The abstraction enables us to reason about the value independent of its resolution.

There was no straightforward way to represent such a value until Promises was introduced in ES6. But the underpinning of a Promise — a wrapper over a state can be emulated by good old callbacks.

Time for a reveal. Consider an ordinary addition.

var sum = x + y;
doSomethingWithSum(sum);

A big assumption hides in plain sight, here. We assume the values of x and y are already resolved.

Suppose we do not know whether x or y contains a value at present. But we know that they eventually will. The user cannot proceed with passing the value sum to doSomethingSum, since they do not know whether the sum is ready yet.

Following our earlier idea of a placeholder, we can conjure a sumPlaceholder and pass the consumer doSomethingWithSum to it.

The code above will work irrespective of the order of these three events

  • the value of x gets resolved
  • the value of y gets resolved
  • the user passes a consumer (doSomethingWithTheSum) to our placeholder object (sumPlaceholder)

The placeholder pattern we employ to abstract time from our program is identified as thunks, more specifically in this case as an asynchronous thunk.

Thunks

A thunk is just a function with a closure state keeping track of some value or values. And it gives them back whenever it is invoked.

Kyle Simpson (@getify)
Rethinking Asynchronous JavaScript, Frontend Masters

Summary

Modern web application needs multiple async operations that interact with each other. These interactions may be required either for message passing or to declare a temporal dependency.

These interactions will be harder without the idea of placeholder ojects. Placeholder objects helps us write code and reason about them without the need to wait for them to resolve to values. Thus, abstracting the idea of time from the program.

The idea can be implemented using plain callbacks which can be thought of as wrapper over a state. This pattern is known as thunks.

ES6 introduced Promises to better adapt to an increasingly async landscape in JS.

The idea of thunk has very little pragmatic use after the introduction of Promises. Although it helps to understand the idea of wrapper over a state which led to Promises.

Note: This article is inspired from the book, Async and Performance, part of the YDKJS book series on JS fundamentals by Kyle Simpson and his course, Rethinking Asynchronous JavaScript on frontendmasters.com.

I have enjoyed and gained a lot from Kyle’s work over the years. If you are discovering him right now and is interested in deeply understanding the fundamentals of JS, I am sure, you will find a great teacher in Kyle.

--

--

Arijit Bhattacharya
Trying to Manipulate the DOM

Self learner, JavaScript performer, Minimal design practitioner, Honest effort admirer. Frontend human @Freshdesk