Skip to main content

Fluent Interface for JavaScript Promises

info
This article was originally published in October 2021 at medium.com. Some formatting might have get lost during the migration to this site: If you think you spotted an issue caused by malformed formatting, feel free to open a Pull Request or send me an Email.

A Fluent Interface is an OOP API using method chaining to increase code legibility. This article explains how to transform JavaScript Promises into more readable code blocks without the constant use of then().

We’ll be seeing the term Promise in this article quite often. Here’s what it means:

A JavaScript Promise represents the eventual completion (or failure) of an asnychronous operation. (MDN)

The Promise-API is part of JavaScript since the 6th Edition of ECMAScript, better known as ECMAScript 2015 or ES6. There are a lot of articles introducing the API, so I do not want to go into too much detail about it here: But if you feel like you need to catch up on things, I recommend reading through the following:

This article and the source code examples assume that you’re familiar with Promises.

Know your tools

The Promise-API was something I took a deep dive in when I found myself in the process of refactoring and migrating the complete code base of my Sencha / ExtJS / JavaScript related projects (coon.js, conjoon, l8js) to NPM in 2021. Automated dependency management is something I wish we had back in the early 2000s (hello, Pear!). I guess the handy work involved with web development back then is responsible for making me feel like that hotshot-knowitall-1337-hacker when I i — save-dev some random package nowadays. And I want my codebase to reflect the modern environment it’s used in, of course.

As with all things that gain your attention (especially if you have more important things to do; also known as procrastination), I got distracted by the tooling. And to ease the process of setting up browser based tests with Siesta (Mats Bryntse, Bryntum), I introduced @coon-js/siesta-lib-helper: A NPM-package that provides easy scaffolding and reusable boilerplate-code for UI/Unit-tests for ExtJS-based projects.

Figure 1 The UI of Siesta, a JavaScript Unit and UI testing tool which can run tests in web pages or in Node.js processes. It integrates extremely well with ExtJS based projects. [Bryntum]

If you’re familiar with Sencha and the ExtJS-framework, you will know that setting up the environment for UI tests can be quite a hassle when using the classic- and/or modern-toolkit, especially since Sencha does not provide dependency management in a way you’d expect from an enterprise-ready JavaScript framework in 2021. But once you've grown accustomed to the pattern of writing tests for it you get a feeling for what needs to be put where for creating a functional testing environment with the right tools…

While a more robust product comes with more to learn, once you know it, productivity increases rapidly. (Sencha)

To get to the point: I wanted my browser based test suites to be magically bootstrapped, and this magic should get profanely applied by an automated process. Sometimes, magic needs to happen asynchronously, so the code had to use Promises (what else than a mere promise would a wish involving magic be?).

And while this approach eliminated almost completely the tedious re-creation of callback hell, I was wondering: Is there a way of getting rid of the - then again - (pun intended) tedious task of explicitly typing then and passing onFulfilled- and onRejected-callbacks as arguments each time I wanted to add another asynchronous task to the Promise chain? How could I make the somewhat fluent interface more liquid?

Source 1 A Promise chain. Granted, arrow functions would make it more readable. Then again, a more fluent interface would make it even more readable.

Fluent Interfaces

Martin Fowler coined the term Fluent Interface along with Eric Evans back in 2005. On his website, he describes the concept – or design approach – fittingly with the API is primarily designed to be readable and to flow. And while fluent interfaces were not invented by Evans nor Fowler, they took care of proper naming and describing the concept to the world.

The API is primarily designed to be readable and to flow. (Martin Fowler)

A fluent interface is a speakable API, and I think most of the readers have come across a fluent interface in their time as a programmer. Take for an example the following code which is part of the query builder of Doctrine:

Source 2 Now that’s what we call a fluent interface!

While it is totally valid to write the query like

Source 3 The same query, in a different variant.

…the first example chains all the methods and does not break the flow of reading the code. While providing a fluent interface does not necessarily provide a functional benefit, it is definitely a more convenient approach for developers when investigating the sources.

A fluent interface forces you to think about what kind of functionality you should break down into smaller functional pieces.

One might want to differ regarding the missing functional benefit. Of course, implementing a fluent interface forces you to think about what kind of functionality you should break down into smaller functional pieces, and therefor expose to the public API. It also adds a grammar to an API, and that helps making its usage in vendor code less error prone. Domain Specific Languages come to mind, and I recommend to read up on what Fowler has to say about it, if you’re interested.

Now imagine writing Promise Chains like this: Instead of using then() as a Higher Order Function (where we are passing the onFulfilled- and onRejected_-_callbacks as the arguments)

Source 4 Another example for a promise chain, using then() as the higher order function it is. Note: We are gracefully and intentional omitting the onRejected-callback here.
we want to write the code like this:
Source 5 Omitting then() in favor of a more readable API.

That should be fairly easy, right? As of the specifications we know that then() returns a Promise (-object!) so we should be good with implementing request(), anotherRequest() and validateResponse()as methods that itself return Promises. We should be able to chain them out of the box. Sounds reasonable. Oh, if it only was this easy…

En attendant Godot

Our naive assumption leads quickly to a first prototype:

Source 6 This does not look fully implemented, but it’s the best we can come up with after somef time where we’re busy with changing code, re-arranging scopes and looking up solution at stackoverflow, all this while pulling our hair out.

There are quite a few problems we stumble upon with this approach. First of, we still would like the code to behave asynchronously. This is why we need to implement the request()-method as a Promise, so it is considered as an asynchronous operation that is thenable by JavaScript.

If the value is a thenable (i.e. has a then()-method), the returned promise will "follow" that thenable, adopting its eventual state; otherwise, the returned promise will be fulfilled with the value. (MDN)

This is how Promises work. They return a Promise itself, indicating the state they’re in (one of _pending_, _fulfilled_, _rejected_). In no simple, obvious nor elegant way we’ll be able to chain one of the members of the given Chain-object to our initial Promise returned by request().

“Hold on”, I hear you say, “I have seen how Promises return another value than a Promise”. You’re somewhat right - you’re probably speaking of the value that was submitted to the resolver (first argument passed to the executor of the Promise), while passing that Promise to the await expression.

Figure 2 Quick catch-up on the Promise API: The argument passed to the constructor is the so called “executor”, which in turn provides two function arguments to control the state (fulfilled, rejected) of the Promise. [MDN]

If a Promise is passed to an await expression, it waits for the Promise to be fulfilled and returns the fulfilled value. (MDN)

Let’s shed some light on this. Here’s some asynchronous code we’ll be refactoring in a second:

Source 7 A typical Promise in its natural habitat. Note how we use setTimeout() to access `result`: This is required since an immediate evaluation of result would return “0”, due to the asynchronous context the code runs in.

Let’s use async/await to fetch the return-value of the Promise inline, instead of chaining the Promise itself by using then():

Source 8

Enough with the excursus and back to our initial problem: Whichever way you look at at it, there is just no way to properly chain Promises in terms of “Fluent Interface” without fundamentally changing the Promise API itself. And since the Promise API is pretty much locked into the JavaScript engine, this is a no-no. If we could only somehow intercept and redefine the behavior of Promises…

Deus ex Machina

Design Patterns are a set of contracts among Software Developers. Their terms and conditions allow for an abstraction of solutions to problems that come up during development, which helps tremendously when facing architectural challenges (“architectural” not in term of DevOps, dear DevOps).

In software engineering, a software design pattern is a general, reusable solution to a commonly occurring problem within a given context in software design. (Wikipedia)

One of the structural patterns that Gamma, Helm, Johnson and Vlissides describe in Design Patterns [📖Gof] (junior devs, take note: that’s a reference work you need to add to your library) is the Proxy Pattern: An representative of another object, providing the same Interface as the proxied object, making it usable in the same way. Although both the Proxy and the target share the same interface, the implementation can differ to a huge degree: While the target object queries a database for data, the Proxy might just return cached values, or delegate the calls to the target, if the cache needs to be updated with actual data. Our use case differs, but in the end, this concept of proxying an object is what we’re going to use. And luckily, it is already part of JavaScript since ES6 as a built-in object.

Figure 3 The UML for the Proxy Pattern. Note how Proxy and RealSubject share the same interface, and Proxy delegates the calls to RealSubject (Source: Wikipedia)

The Proxy in JavaScript

Creating a Proxy in JavaScript is done by calling a Proxy constructor with two arguments: The target object that should be proxied, and the handler, implementing behavior and trapping calls to the target object: The target object gets wrapped by another object — a common concept that a lot of the structural patterns share.

Figure 4 The Proxy constructor in JavaScript. It takes the target object and a handler as arguments, whereas the handler implements the behavior [MDN]

Now you might wonder where the additional abstraction layer can be found, since we have seen that the Proxy and the target object share the same public API. After all, the Proxy must be aware of the contract it has to fulfill to comply with the interface of the target object, right?

The problem with object-oriented languages is they’ve got all this implicit environment that they carry around with them. You wanted a banana but what you got was a gorilla holding the banana and the entire jungle. ([Joe Armstrong](https://en.wikipedia.org/wiki/Joe_Armstrong%28programmer%29))_

Proxies in JavaScript work different: JavaScript is not a class-based OO-language - it’s missing the concept of abstract classes and interfaces. It is loosely typed and although it was invented to ride along with the hip bandwagon named Java back in the days - it somewhat looks like it, it sometimes feels like it (looking at its syntax), but it simply is not Java.

Figure 5 You will find a lot of “like Java”-related quotes when looking at the history of JavaScript. [Brendan Eich @Twitter]

In JavaScript, calls being trapped by a Proxy’s handler, making it possible to virtually extend the target object with logic, provided that the behavior for those calls is implemented in the handler itself. Let’s take a look at an example and get into the details of traps afterwards.

Source 9 Setting/getting properties of a target object trapped by a Proxy and its handler.

Step 1: We are defining a target- and a handler-object, both blank with no prototype inheritance (hence Object.create(null) — you can go ahead and use the object literal {} instead, it wouldn’t make a difference in our example). The handler has two methods — get() and set().

Step 2: After we have defined both objects, we’re creating a Proxy out of both — with target being the target object, and handler being the behavioral defined.

get() and set() are template methods and executed if they are available in the handler. The implementation is up to us (note: this goes for the complete set of handler functions available, of course).

Figure 6 The syntax for get() [MDN]
Figure 7 The syntax for set() [MDN]

Step 3: We then assign a value to proxy.say— the setter-call is trapped by the set()-method defined in the Proxy, executing its implementation.

Step 4: The same goes for get. When accessing a property of the trapped object, the implementation of the Proxy’s get() — method is called.

The arguments passed to both methods provide the appropriate meta-information about the context. In both, we find target, property, receiver — the naming should be obvious, except for receiver. To shed light into the dark: Here, receiver references the Proxy-instance.

A Proxy in JavaScript does not magically trap method calls on a target object.

Now, before we get too excited about it — there is one catch: Calling methods on a Proxy. Contrary to the believe, arbitrary method calls on a Proxy would automatically be trapped by some kind of magic methods (just like in PHP), JavaScript handles it in a different way.

Source 10 Magic methods in PHP. Calls to inaccessible methods on instances of the class “MethodTest” trigger __call()

Actually, methods that should be trapped by a Proxy must be caught by the get()-implementation of the Proxy itself — and return a callable. Let’s see how the PHP example from above works with JavaScript:

Source 11 An implementation of PHP’s __call() in JavaScript. The arguments are not available to the Proxy’s get()-method, since the trap snaps when the method is looked up, not when it’s called.

However, the Proxy API allows for a way to also trap method calls, which seems to be a little bit cumbersome at first glance: Let’s have a look at the apply()-method:

Figure 7 The apply()-method of the Proxy API allows for trapping method calls. [MDN]

In order for apply()to work, we are now passing a function as the target, and call the proxy itself as if it was a function. That differs from the previously introduced use case of proxying an object and might be confusing, but we’ll get the hang of it shortly:

Source 12 Calling a Proxy on a trapped function triggers the handler’s apply()-method.

In the given example, the target is an arrow function that simply logs “I was not trapped” to the console. It’s behavior gets changed by the trappedTarget-Proxy and its handler, which — once called — produces the output “I was trapped”.

So, how, when and where does this make sense?

A trapped function call is aware of its context: The target itself, i.e. the original function being proxied, the thisArg, which is the object the call is bound to, and the argumentsList, containing all arguments passed to it.

Source 13 Forwarding information from the trapped function-call to the original function. We have changed from arrow functions to traditional function expressions to make use of call() and the arguments-object.

Forwarding information from the trapped function-call to the original function. We have changed from arrow functions to traditional function expressions to make use of call() and the arguments-object.

Let’s make more sense of it:

  1. We can intercept method calls when that method is looked up on the Proxy — via get()
  2. We can use a Proxy for method calls, effectively changing the expected behavior by processing a different implementation — via apply()

Both concepts are chained together (pun intended, buckle up!) in the following example. Let this one sink. You should get the idea where we’re heading with this:

Source 14

Here’s what’s happening: Step 1:trap(obj)

create Proxy for obj

Step 2.1:trap(target).foo

foois trapped and looked up in get(); get()returns obj.foo() as a proxied function, using the same handler as obj

Step 2.2:trap(target).foo()

The handler’s apply() method is called, serving as a Proxy for the foo()-method. The original foo()-method is available as the target-argument. This method is called, and the return value is logged on the console, producing “The Answer to the ultimate question of life, The Universe and Everything is…” as output. Contrary to the original implementation of foo(), its Proxy returns an object, thisArg, referencing the Proxy created for obj. If you’re struggling with this: trap(obj).foo()takes care of calling foo in the scope of the object trap(obj) represents — the Proxy.

Step 3.1:trap(target).foo().bar

bar is trapped by the return value of the proxied foo()-method (see 2.2. for the explanation) and looked up with get(); get() returns obj.bar as a proxied function, using the same handler as obj.

Step 3.2: trap(target).foo().bar()

the handler’s apply()-method is called, serving as a Proxy for the bar()-method. The original bar()-method is available as the target-argument. This method is called, and the return value is logged on the console, producing “42!” as output. Again, its Proxy returns an object, thisArg, referencing the same object our proxied foo() returned.

We have just created a Fluent Interface for an API that was not originally implemented as such. And I think that’s beautiful!

It’s finally time for the Fluent Interface implementation for Promises…

A Proxy for creating Fluent Interfaces for Promises

The liquify()-Proxy is part of l8js, a lightweight core JavaScript library that skips abstraction layers for the sake of a more lean approach towards functional programming.

The implementation follows the concept showcased in the previous example, where we effectively change the behavior of a Promise by advising the Proxy to trap API-calls in the get()- and apply() -methods of the handler.

Here’s its implementation:

Source 15

Implementation Notes: This variant requires all of our async methods to return this, i.e. the owner of the asynchronous methods, so that the onFulfilled-callbacks forward this exact owner to the next link in the chain without too much of a hassle.

For example, the following will throw:

const source = {
foo: async function () { return this; },
bar: async function () { return "somerandomstring"; },
^^^^^^^^^^^^^^^^^^
snafu: async function () { return "done"; }
};

await liquify(source).foo().bar().snafu();
// will throw an error since "snafu()" cannot be looked up anymore

We will step through an example and inspect the various steps to get a better understanding of how this proxified Promise works:

const source = {
foo: async function () { return this; },
bar: async function () { return this; },
snafu: async function () { return "done"; }
};

await liquify(source).foo().bar().snafu()

Step 1: liquify(source) This call will create a Proxy for source, with handler trapping further calls.

Step 2: liquify(source).foo … is trapped by the handler’s get()-method. Returns a bound(!) function (proxied again by liquify()), with target referencing source, and the property being "foo"

return liquify(target[property].bind(target))

Step 3: liquify(source).foo() The previous call to liquify(source).foo returned a proxied, bound function. At this point, the method call foo() is now trapped in the Proxy’s apply()-method. The returned Promise is proxied again.

return liquify(target.apply(thisArg, argumentsList))

Step 4: liquify(source).foo().bar Step 3 returned a Promise, so bar as a property is now looked up on the Promise. The problem is, of course, that the Promise does not have a property called bar. We now have to take care of chaining the source-object through, so the following method call can properly resolve to source.bar. We do this by implementing the fulfilled-callback. The get-handler will check if the target owns a then-method and return the following:

liquify(target.then(value => value[property].bind(value)));
^^**1***^^ ^^**2***^^ ^^^^^^^^^ **3***^^^^^^^^^^
  • 1* the Promise that was proxied in step 3
  • 2* value is the return-value of the original source.foo()
  • 3* property is known to the implementation of the fulfilled-callback when it gets called (see: lexical scope). The return value of this callback is the method bar, bound to source, its owner.

Step 5: liquify(source).foo().bar() The apply-handler now traps a callable. Since we have previously returned a Promise, and a Promise is not a callable method, we help ourselves with a clever trick: We are not directly wrapping the argument passed to liquify with the Proxy, but rather create function that is called. We “tag” this function with the property __liquid__ that helps the handler to identify a proxied, callable method:

let promise = new Promise()
liquify(promise);
function liquify(target) {
let cb = function () {
return target;
};
cb.__liquid__ = true;
}
return new Proxy(cb, handler);

What happens now is that this exact function is processed by the apply()-handler: bar() calls cb(); cb() returns a Promise, the apply-handler makes sure the fulfilled-callback is implemented, and returns the resulting Promise wrapped in a Proxy.

liquify(promise.then(value => Reflect.apply(value, thisArg, args)));
^^**1***^^ ^^^^^^^^^^^^**2***^^^^^^^^^^^^^^
  • 1* the bound method that was returned in the fulfilled-callback implemented in Step 4
  • 2* The return value of the fulfilled-callback, which, in this case, is the call to source.bar() It is important to use args here since it holds are the arguments referencing the resolve/reject-callbacks used by the last call in the chain

Step 6: then() The last call in the chain is an implicit call to then() triggered by the Promise-instance that was proxied in Step 5. No more links (read: properties) have to be looked up — the chain ends at this point. then is a property on a proxied Promise, so the handler traps it and simply binds the method to the Promise. The expression of the return statement of async bar() is returned, which equals to "done".

Summary

Proxies provide a powerful tool to change the behavior of existing functionality, even when dealing with a locked API.

Figure 8 A liquified Promise-chain helping to set up a and run a test suite with @coon-js/siesta-lib-helper

Of course, we’ve merely scratched the surface regarding Proxies. There are still, inter alia, methods like set(), preventExtension() or Proxy.revocable(), which allows for switching off a Proxy during runtime. If you’d like to know more, head over to MDN, then take your time to read through A practical guide to JavaScript Proxy, which gives a good overview of the advanced concepts of Proxies. And of course, I’m not the first one who dealt with Fluent Interfaces and Promises. For example, have a look at the implementation of Ilya Kozhevniko‘s proxymise here.