Sometimes, when you really want to understand something, it’s worth trying to build it yourself. That is what I do with promises in this article. I am trying to build a very basic ‘Promise’.
Let’s begin with the basics, before anybody heard of promises, we had callbacks. Take a look at the following snippet.
const testUrl = 'https://api.github.com/users/odemeulder'const fetchData = (url, callback) => {let xhr = new XMLHttpRequest()xhr.onreadystatechange = () => {if (xhr.readyState == 200 ) {callback(xhr.responseText)}}xhr.open(url)xhr.send()}fetchData(testUrl, response => console.log(response) )
What does it do? It creates a very simple function that takes a url, reads it using the XMLHttpRequest
and allows a callback function to do something with the result. And in the last line, we call our new function; as a callback we provide a function that prints out the response. Very simple. If you are not familiar with XMLHttpRequest
, don’t fret, it is not important to understand for the rest of the article. Just know that it is the object that the browser provides to JavaScript to perform http
requests. It is an asynchronous function, it takes a callback when the onreadystatechange
event fires.
Before diving into writing our own promise, let’s see what the end result should look like. What is the call signature?
function fetchDataReturningPromise(url) {// what happens here?}
function functionWithDelay(arg) {// what happens here?}
fetchDataReturningPromise(url).then(response => console.log(response))// orfunctionWithDelay(arg1).then(performFollowUpOperation)// orfunctionWithDelay(arg1).then((resolvedValue) =>PerformFollowUpOperation(resolvedValue))
What do we have here? Let’s focus on the third example.
We have a function with a delay; something inside the function makes an asynchronous call. It could be a timeout, an http request, a database call.
That function returns ‘something’. That something has a then
method, so ‘something’ must be an object. In fact ‘something’ will be a function.
The then
function takes one argument, in the form of a function or a callback. And the callback passed into the then
function does not get executed until functionWithDelay
is done. In our third example the function we pass to then
is labeled performFollowUpOperation
. Note that performFollowUpOperation
takes one argument (resolvedValue
) and that argument is provided by functionWithDelay
. Lots to unpack here. Pause, and re-read previous paragraph (if you’d like to).
In summary:
2. Define a function with a delay that returns a function with a next function (functionWithDelay
)
3. The function with a next
function needs to take another function as an argument. That follow-up function cannot be executed until the function with delay is done. (performFollowUpOperation
)
4. The function with delay must somehow pass a value to the follow-up function.
So now we need to fill in functionWithDelay
, first we need to define that thing that has a next
function.
Let’s make a first attempt.
1: function FunctionWithAThenFunction() {2: this.then = function (followUpFunction) {3: followUpFunction()4: }5: }
6: function functionWithDelay(param1) {7: let xhr = new XMLHttpRequest()8: xhr.onreadystatechange = () => {9: if (xhr.readyState == 200) callback(xhr.responseText)10: }11: xhr.open(param1)12: xhr.send()13: return new FunctionWithAThenFunction()14: }
All right, we have a function with a then
function. Our functionWithDelay
returns a function with a then
function. But. What is callback
on line 9? Somehow that followUpFunction
should end up in the callback. That is not happening. Therefor, the followUpFunction
is going to execute right away and not wait until functionWithDelay
is done.
How do we get that followUpFunction
to be callback
?
Let’s take a look at functionWithDelay
. What is characteristic about a function with a delay? Or what is characteristic about asynchronous functions? They will often take a callback in some way. So let’s try to re-write that function as accepting a callback, such that we can access that callback easily.
And then let’s rewrite our FunctionWithAThenFunction
and let it accept an function, more specifically an asynchronous function with callback.
1: function FunctionWithAThenFunction(aFnWithDelayAndACallback){2: let nextThingToDo3: this.then = function ( followUpFunction) {4: nextThingToDo = followUpFunction5: }6: aFnWithDelayAndACallback( nextThingToDo )7: }8: function functionWithDelay(param1) {9: const httpRequestWithCallback = function (callback) {10: let xhr = XMLHttpRequest()11: xhr.onreadystatechange = () => {12: if (xhr.readyState == 200) callback(xhr.responseText)13: }14: xhr.open(param1)15: xhr.send()16: }17: return new FunctionWithAThenFunction(httpRequestWithCallback)18: }19: functionWithDelay('some argument').then(console.log)
Lines 1–7 define our FunctionWithAThenFunction
. That is the function that will be returned by the function with a delay. And it expects a function as an argument. More specifically a function with a callback. The then
function then sets the callback in a local variable (nextThingToDo
). And that function really does one thing and that is to call the function with the callback, in our example the function labeled aFnWithDelayAndACallback
.
Lines 8–18 define our functionWithDelay
. In our example the function that does an http
request and returns something with a then
function. And that something with a then
function is a new instance of FunctionWithAThenFunction
. It looks a little different than what we did above. We create a new local temporary function named httpRequestWithCallback
, which accepts a callback argument and wraps the entire XMLHttpRequest
code from the prior examples. That new local function is then passed as an argument to FunctionWithAThenFunction
.
Now, if you try to run this (line 19), it will still not accomplish what we are trying to do. If you follow along, you will see that nextThingToDo
is undefined
as we are calling it before we get a chance to assign it a value. We are missing one thing and that is some kind of status.
Let’s add a variable that keeps track of the status of execution.
1: function FunctionWithAThenFunction(aFnWithDelayAndACallback){2: let nextThingToDo3: let status = 'pending'4: this.then = function ( followUpFunction) {5: if (status === 'pending') {6: nextThingToDo = followUpFunction7: }8: else {9: followUpFunction()10: }11: }12: aFnWithDelayAndACallback( (newValue) => {13: status = 'resolved'14: nextThingToDo(newValue)15: })16: }17: function functionWithDelay(param1) {18: const httpRequestWithCallback = function (callback) {19: let xhr = XMLHttpRequest()20: xhr.onreadystatechange = () => {21: if (xhr.readyState == 200) callback(xhr.responseText)22: }23: xhr.open(param1)24: xhr.send()25: }26: return new FunctionWithAThenFunction(httpRequestWithCallback)27: }28: functionWithDelay('some argument').then(console.log)
You see that we declare a status
variable (line 3). This way we cover all of our basis. If then
is called before the main function is done, we store the followUpFunction
in a variable, if not we can safely execute it. But it does not have the value from the main function. Let’s fix that.
1: function FunctionWithAThenFunction(aFnWithDelayAndACallback){2: let nextThingToDo, value3: let status = 'pending'4: this.then = function ( followUpFunction) {5: if (status === 'pending') {6: nextThingToDo = followUpFunction7: }8: else {9: followUpFunction(value)10: }11: }12: aFnWithDelayAndACallback( (newValue) => {13: status = 'resolved'14: value = newValue15: nextThingToDo(newValue)16: })17: }18: function functionWithDelay(param1) {19: const httpRequestWithCallback = function (callback) {20: let xhr = XMLHttpRequest()21: xhr.onreadystatechange = () => {22: if (xhr.readyState == 200) callback(xhr.responseText)23: }24: xhr.open(param1)25: xhr.send()26: }27: return new FunctionWithAThenFunction(httpRequestWithCallback)28: }29: functionWithDelay('some argument').then(console.log)
Here we declare a new local variable named value
. (line 2) When we resolve the main function we set that value. So it can be passed as a parameter to followUpFunction
on line 9.
Speaking of resolving, let’s add re-write this for clarity. No change in functionality with the previous sample.
1: function FunctionWithAThenFunction(aFnWithDelayAndACallback){2: let nextThingToDo, value3: let status = 'pending'4: this.then = function ( followUpFunction) {5: if (status === 'pending') {6: nextThingToDo = followUpFunction7: }8: else {9: followUpFunction(value)10: }11: }12: const resolve = newValue => {13: value = newValue14: status = 'resolved'15: if (nextThingToDo) nextThingToDo(newValue)16: }17: aFnWithDelayAndACallback(resolve)17: }18: function functionWithDelay(param1) {19: const httpRequestWithCallback = function (callback) {20: let xhr = XMLHttpRequest()21: xhr.onreadystatechange = () => {22: if (xhr.readyState == 200) callback(xhr.responseText)23: }24: xhr.open(param1)25: xhr.send()26: }27: return new FunctionWithAThenFunction(httpRequestWithCallback)28: }29: functionWithDelay('some argument').then(console.log)
Here we introduce the local resolve function for clarity on lines 12–16. It is subsequently called on line 17.
We’ve come a long way. Let’s re-write the last example one more time and change the function names a little bit.
FunctionWithAThenFunction
becomes Promise
functionWithDelay
becomes fetchData
httpRequestWithCallback
becomes httpRequest
aFnWithDelayAndACallback
becoms fn
function Promise(fn){let nextThingToDo, valuelet status = 'pending'this.then = function ( callback ) {if (status === 'pending') {nextThingToDo = callback}else {callback(value)}}const resolve = newValue => {value = newValuestatus = 'resolved'if (nextThingToDo) nextThingToDo(newValue)}fn(resolve)}function fetchData(url) {const httpRequest = function (callback) {let xhr = XMLHttpRequest()xhr.onreadystatechange = () => {if (xhr.readyState == 200) callback(xhr.responseText)}xhr.open(url)xhr.send()}return new Promise(httpRequest)}
const testUrl = 'https://api.github.com/users/odemeulder'fetchData(testUrl).then(console.log)
The Promise
code can now be used in other functions. Let’s say you wanted to have a function similar to setTimeout
that returned a promise. Let’s call the function Wait
and it would accept a delay time in milliseconds.
function Wait(milliSeconds) {const fn = (callback) => setTimeout(callback, milliSeconds)return new Promise(fn)}Wait(5000).then(() => console.log('we waited 5 seconds'))
Or let’s say you want to use the httpRequest
example in Node. In Node XmlHttpRequest
won’t work because that is an object provided by the browser, you’d have to use Node’s built in https
module.
const https = require('https')const testUrl = 'https://api.github.com/users/odemeulder'function fetchDataForNode(url) {const fn = callback => {const options = {url: url,headers: { 'User-Agent': 'request' }}https.get(options, response => {let retresponse.on('data', chunk => ret += chunk)response.on('end', () => callback(ret))})return new Promise(fn)}fetchDataForNode(testUrl).then(console.log)
Again, don’t fret over the technicalities of how https.get
works in node. The important part is: create a function that takes a callback and wraps an asynchronous call, and pass that function to your Promise
constructor.
There is obviously much more to a promise than what we just outlined. This article is long enough already. But here are some improvements / additions.
One thing is that the then
function should return a new promise. That allows you to chain promises. doSomething().then(somethingElse).then(aThirdThing)
And the other big thing that we did not go into is error handling. Our Promise
function should also implement a catch
function. That function would take a function as a parameter and that function would be called in case of any errors.
We created a very basic Promise object, and I showed you three examples of how to create a function that returns a promise (fetchData
for browser http requests, fetchDataForNode
for http requests in Node and wait
). In each case, you create a function that accepts a callback, create a new Promise
function and pass the function as callback.
I welcome any feedback as well as applause.