Fluent Interface for JavaScript Promises
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.
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?
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:
While it is totally valid to write the query like
…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)
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:
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.
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:
Let’s use async/await
to fetch the return-value of the Promise inline, instead of chaining the Promise itself by using then()
:
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.
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.
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.
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.
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).
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.
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:
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:
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:
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.
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:
- We can intercept method calls when that method is looked up on the Proxy — via
get()
- 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:
Here’s what’s happening:
Step 1:trap(obj)
create Proxy for obj
Step 2.1:trap(target).foo
foo
is 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:
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 thefulfilled
-callback when it gets called (see: lexical scope). The return value of this callback is the methodbar
, bound tosource
, 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 tosource.bar()
It is important to useargs
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.
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.