I Promise you a Miracle

Written by odemeulder | Published 2017/09/05
Tech Story Tags: javascript | promises | programming

TLDRvia the TL;DR App

I am rolling my own JavaScript promise.

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’.

At first there were callbacks …

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.

What does a promise look like?

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:

  1. Define a function with a next function.

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.

First naieve attempt

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?

Second attempt

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.

Third Attempt

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.

Putting it all together

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.

Next Steps

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.

Conclusion

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.


Published by HackerNoon on 2017/09/05