The busy developers guide to (a)synchronicity and promises in Javascript.
A brief overview of the bigger picture.
Lately, there has been somewhat of a resurgence in the need to (re)explain Promises in Javascript. I'm not sure if this is because they remain some elusive magical trinket of the language or if people are still not grasping what they are, or why they are needed. On this, I assume the problematic nature lies within the realm of the beginner to the language, or even to writing software altogether.
By now promises, futures, async functions are a common feature of many programming languages. The asynchronous paradigm is arguably prominent daily in most developers' lives in some form or other. Therefore it seems pertinent of me to try and give a brief (er) overview for those developers who need no more than a quick look to clarify issues.
I'll start by explaining as simple as possible what asynchronicity is in general, and then how promises and async patterns work in javaScript. Why Javascript? well for many reasons, the least of which is its prominence, popularity, but mostly because asynchronicity and promises, have long been a feature in javaScript, and now more recently Promises and async behavior is baked into the core language.
Concurrency and the Event Loop
If one Wiki's this undoubtedly you would stumble across a myriad of explanations, both computer software related and un software related. So in the spirit of this article, I shall provide the briefest explanation I can, and in javascript, we cannot ignore these as core facets of the languages concurrency model, and in particular the "event loop"
The best way to illustrate this is to cite Mozilla's developer documentation, where is states:
JavaScript has a concurrency model based on an event loop, which is responsible for executing the code, collecting and processing events, and executing queued sub-tasks.....
Basically, consider this a simple (single) thread of execution going round in a circle (loop) and collecting (queuing) jobs (functions) processing (executing) them, and then continuing on its merry way. It collects them and adds them to a "stack" a first in last out memory space queue where it places the most recent task on the top, executes it then removes it. Think of it as a to-do list where the newest tasks are added to the top of the list, and removed when done from the top down.
The core takeaway from this is that this loop is in order, synchronous, or concurrent (one after the other). It needs to run each function or task to the end before undertaking the next one. This essentially "blocks" the execution of the other functions until the current one is completed. So, the order of queue "a,b,c" is c -> b -> a, in other words, a will not be completed until b is completed and b not until c is completed. This blocking synchronous behavior is why in the not too distant internet past, pages would reload after tasks, like a form submission, as the task or job that was responsible for sending the form data to the server for processing would have to wait until the server returned something before continuing on. For those of us who remember this, it wasn't the best experience!
This was and is due to the fact that JavaScript runs in a 'thread' of execution, a single loop that has to do all the work. In some other languages, there are greater opportunities for splitting this work up into separate threads to overcome blocking natures. This is known as multi-threading, or sometimes multi-tasking though there subtle differences with these terms. Javascript is evolving and there are now ways around this, using web-workers in the browser, or child processes on the back end (for example in Node), however, the core language, remains a single-threaded process.
Asynchronicity
This blocking single-threaded nature of javascript led to the development of features in the language which would enable a function to be queued (scheduled) for execution, and executed when the next available event loop iteration could execute it, but in the meantime, other tasks could be executed regardless of the completed order. This is known as "asynchronicity" essentially, the opposite of synchronicity. Here, the task is queued with a contract or "promise" that at some stage in the event loop, its unit of work usually known as a callback will be executed. This callback is stored in a particular space in the stack known as the The Callback Queue. All this is is a queue of no particular order of execution, from which tasks will be picked off whenever the event loops scheduler decides it's free.
So, going back to our A, B, C example above, the asynchronous approach to this no longer guarantees C -> B -> A order of execution, it could be any iteration of this. This may seem odd, but from the perspective of the overall flow of execution of the application thread, this ensures at least minimally, that nothing will be blocked. So in the case of our form submission, the user will be able to continue using the site in the browser once they have submitted the data, and not have to await a response.
The above was a very generalized precis of concurrency and (a)synchronicity in theory. There is slightly more to it than that, and there are many great resources available for learning more. However, this guide is intended as a brief(ish) overview for busy developers, so I artistically omit the finer details, for the sake of my alleged brevity. Yet it suffices as a lead into how such synchronicity is implemented in JavaScript, here we look predominantly at promises, a feature of Javascript browser behavior.
Promises, promises, promises.
Javascript manages asynchronicity in a number of ways. From the non-browser domain, (e.g. server-side, using the term loosely). Here, asynchronicity is now available with many core features of Node, particularly when undertaking OS level IO operations, managing files, and directories. This arises in the form of asynchronous versions of previous synchronous functions and methods. For example, the highly cited
readFile()
function in Node, also has a readFileAsync
sister function, which wraps up the core behavior at the OS level in a callback implemented async feature set. Predominantly, however, the common approach to asynchronicity in JavaScript is the use of the Promise behavior. Promises are JavaScript implementations of the standard approach to indicating a function, method, or equivalent unit of work, is destined for the callback queue, and not to expect (assign) the result to any reference that is required immediately after execution.
Promises are not particularly new, the syntactical language construct Promise is fairly new, but this is merely a pretty and clear way to implement the callback future contract behavior. In fact, in the browser Promises are commonly a way to better create the “Asynchronous JavaScript and XML (AJAX) pattern,that was formed many moons ago. Back in the day, it was a bit of a coding feat to keep things simple especially considering each browser had varying amounts of support for it, which required the developer to write checks for!
Thankfully nowadays, it's merely a standardized Promise, under the hood, there can still be mechanics going on depending on whether one is using an external library such as Axios, the core javascript fetch api , or the core Promise implementation in Javascript. Regardless of this, the concept remains the same, a Promise is as it suggests, it is a contract with the event loop that at some stage in the cycle this unit of work will be fired, it will be called back into the main thread, and return something. It will not happen immediately but it will at some stage, "I promise".
Promise implementations in Javascript.
As mentioned above, asynchronicity is not (any longer) exclusive to the browser, since Node, with the V8 engine and Libuv magic, vast amounts of servers, and platform logic all over the world is written in Javascript, where once it may have been Java, C# or similar. However, notably, the largest use of Promises remains in the browser and will do so as long as JavaScript remains almost the sole language of the web. Therefore the following illustrations and examples relate largely to the browser.
The most common approach to Promises in browser code is with the use of the "Promise" class/object approach. Either with a third-party library or with javaScripts own Promise. With the third-party approach, it's common that the actual implementation is encapsulated in exported functions that return a promise. For example, commonly Promisified functions are often named after the (REST) like actions they express, such as get, post, update etc. However, this is not a standard, nor a necessity, but it is frequently seen in browser implementations. Taking a common get expression it is commonly holding a signature as follows. (pseudocode)
get(endpoint, callback: <Function(result)>, options): Promise
Thus implementation of the above would be:
get('some_end_point',#options)
.then(result->)
.catch(error ->)
The signature above from the implementation suggests a promise by the use of the then() callback. This is saying Once the work is done (whenever, return the data to me in the callback, or in the case of an error, please return the error). The catch is optional here, but usually good practice.
Here, the function get()
is explicitly returning a promise. This is not always necessary, an existing function can be 'promisified' by wrapping a return value in a promise, more on that in a bit. Also common, is the pure implementation of a Promise, using JavaScript's own Promise constructor. (pseud code)
const myPromise = new Promise((resolve, reject) => {
const result = #someaction()
if(result) {
resolve(result)
} else {
reject(new Error(error))
})
Then from the above, the implementation can be
myPromise().then(res -> ...).catch(e -> ...)
Both of these examples, consist of a function that returns a Promise. In the explicit use of the Promise constructor, the callback carries two functions resove, and reject. These are fairly self explanatory, depending on implementation, resolve()
passes the positive result back to the then()
callback function reject()
to catch callback or catch if in a try / catch block (see below)
There is nothing magical or overly complicated in this, both signature examples merely implement a contract to the event loop (in this case the browser event loop) to queue the action to the callback loop, and process it at some stage, once processed return the corresponding value back to the calling thread function, a value to the callback, or an error if and as handled.
Callback "hell"
Of course, one drawback of callbacks is if one requires an interdependency on the returned value, for another callback. In this sense, it's similar to attempting to gain synchronicity of asynchronous code. This is clear if code, slips into a long chain of callbacks, often termed "callback hell".
This is a fairly derived example, but I am sure you get the point. It works, but extensibility and maintenance would not be an easy task here, certainly! There are several approaches to avoiding this chaining issue. Some are supplied by third-party libraries, and some solutions are core to modern Javascript. One is the, promise.all()
approach, which takes an array of async functions, and returns the collaborative values. This is a common pattern in Axios for example but is also possible in core Javascript.
const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, 'foo');
});
Promise.all([promise1, promise2, promise3]).then((values) => {
console.log(values);
});
// expected output: Array [3, 42, "foo"]
Of course, here all functions must be declared and promisified (return a promise), but the approach is much cleaner than chaining as you see.
Another approach, which I favor, is to wrap your asynchronous code in an async/ await scope. This essentially proimisifies a method, but in my opinion, is far more succinct. The rules are that the 'async' keyword is added to the signature, and in doing so the 'await' promise indicator must be provided. The activity should be wrapped in a try/catch block, particularly if requiring good method handling. The signature of an async / await function is as follows: (pseudocode)
async function myPromisifiedFunction(params){
try. {
const result = await fetch(url)
return result / json / callback with result
} catch (error) {
return new Error(error) / or callback with error
}
}
Then implementation is the same as any Promise if needed, Or if the function doesn't return the result, a callback can be passed, or even the result can be assigned to another await.
// as another await
const myResult = await myPromisifiedFunction(params)
// With callback
myPromisifiedFunction(callback)
// As a Promisified function
myPromisifiedFunction
.then(result ->)
.catch(error ->)
Bearing in mind these are all Promises, and all behave asynchronously, however, syntactically, they can be implemented differently, and provide cleaner ways of implementing any potentially overly obfuscated and complex functionality.
There is also a newer implementation of the Promise approach, which gives greater control over the flow of a promise. This approach is the "generator function" In this case, the control over the iteration of data can be paused and returned (yielded) at points. However, this is a little different use case, and perhaps better covered in another post. If you are interested, you can read more about them here
In Conclusion
Promises are a key facet of a developer's toolkit. In Javascript, in particular, they are a frequent requirement, especially with Web applications. Here, I have merely attempted to provide, a clear overview of what, why, and how they operate. Also, remember key points to take away.
- Javascript is by default synchronous and single-threaded and blocks operations
- The event loop is the foundational operation in Javascript,
- The Stack and Callback Stack is where tasks are queued and handled
- Asynchronicity must be managed via code
- Promises are a contract for work at some stage in the loop
- Promises are an implementation of asynchronicity that features in all domains both front end and back -Javascript manages Promises through the Promise Constructor, Async Await, Generators, all of which implement the Promise behavior/interface.
- It's good practice to handle rejections and errors in promise callbacks but not necessary.