As a front-end developer, most of my time is spent inside a framework that gives me access to a lot of modern features. There are however moments where I have to step outside of my framework. Lo and behold, these moments are the points in time that remind me of all the things I take for granted. Promises is one of these things.

In this post, I walk through coding up a simple implementation of promises. The goal is to provide a deeper understanding of promises by looking into how they are used to wrap asynchronous events. Through this exercise we develop a greater appreciation for frameworks that gives us modern features (like promises) as well as the standards that eventually bring features like these into future versions of the language.

The first part of this writeup describes what it means to be asynchronous, how they can lead to confusing code, what promises are and how it can help make code a little less confusing. Feel free to skip to the implementation if you are already familiar with using promises.


Async via Callbacks

The traditional approach to asynchronous events is callback functions. Using methods like addEventListener, code is executed in response to an event. While this works… this approach starts becoming messy when code that responds to asynchronous events also produce asynchronous events.

Take for example:

A button is clicked which triggers a file upload. The upload completes which then triggers a database save. The database gets updated which then sends an alert to the user.

The bolded items are things that are asynchronous.

A button click is asynchronous because there is no guarantee when the button will be clicked. Code handling the button click is only triggered when a user decides to click on the button. When this button is clicked, a file upload occurs. This is also asynchronous because it is uncertain when the upload will finish. Because of this, we cannot determine exactly when to proceed to the next step (saving). The act of saving is also asynchronous because it can take a variable amount of time to finish. Are we saving a large document or a small document? Different answers yields different results which makes it also impossible to determine when to alert the user.

By now you see that asynchronous events are basically linked to some degree of uncertainty.

We don’t know when something will happen, but when/if it does, we want these other things to also happen.

Callbacks and methods like addEventListener deals with this.

Async with Callbacks

Using the example above, a callback approach might look like the following:

var button = document.querySelector('button');
var myUploader = new MyUploader();
var database = new Database();

myUploader.addEventListener('complete', function() {
  database.save('new data to save');
});

database.addEventListener('saveComplete', function() {
  alert('done saving');
});

button.addEventListener('click', function() {
  myUploader.upload('myfile');
});

Notice that this piece of code is not very easy to understand “just by looking” at it. If you did not know about the problem in advance, just figuring out that the button click is the “starting point” of all of this code execution can be challenging. The problem is that event listeners in general can be set up in any order. I explicitly did it in reverse order in the example to show how event handling via callbacks can lead to hard to understand code. Thankfully we have mechanisms like promises that helps us write slightly cleaner code.


Promises

A promise is a mechanism that allows us to write asynchronous code in a way that looks synchronous. We want this because synchronous code in general looks cleaner and is easier to understand.

Before diving into functions on a promise object, here are some examples of a promise being used.

Using Promises

Async via (event + promise)

We use the upload example from before, but this time implemented using promises.

var button = document.querySelector('button');
var myUploader = new MyUploader();
var database = new Database();

button.addEventListener('click', function() {
  myUploader.upload('myfile')
    .then(function() {
      return database.save('new data to save');
    })
    .then(function() {
      alert('done saving');
    });
});

The promise approach is a lot less confusing because it reads linearly. We still need to write an event listener because that is how DOM events work, but we use promises for our uploader and database classes instead of event registration / callbacks. In this example, myUploader.upload returns a promise which is then used to chain other pieces of code to run in linear fashion.

Basically promises allows us to write code in a way that is…

When this happens,then this happens, then this happens…

The then function in promises can also accept a second argument that is a function that deals with errors. From the previous example, if I wanted to handle any errors from the database save, I pass in a second function which deals with errors.

Promise (Success + Error Handling)

...
button.addEventListener('click', function() {
  myUploader.upload('myfile')
    .then(function() {
      return database.save('new data to save');
    })
    .then(
      function() {
        alert('done saving');
      },
      function () {
        alert('error saving!');
      }
    );
});

Writing Code that returns a promise

A promise have both a resolve and a reject method. These two methods directly relate to the success and failure functions passed into the then method.

When RESOLVE is called, the success handler is invoked. When REJECT is called, the failure handler is invoked.

Using the database save example, the internals of our database class might look something like this:

database.prototype.save = function(data) {
  var promise = new Promise();

  // Keeps track of an internal _db object
  // that does the actual saving to a database

  // resolve when save is done
  _db.addEventListener('saveComplete', function() {
    promise.resolve('save success');
  });

  // reject when save fails
  _db.addEventListener('saveError', function() {
    promise.reject('save failed');
  });

  // go perform the actual asynchronous saving
  _db.save();
  return promise;
};

Promise Implementation

Now that we know what promises are and how to use them, we shall now implement promises.

To recap on how promises are used, we bring back the database save example:

database.prototype.save = function(data) {
  var promise = new Promise();
  // resolve when save is done
  _db.addEventListener('saveComplete', function() {
    promise.resolve('save success');
  });
  // reject when save fails
  _db.addEventListener('saveError', function() {
    promise.reject('save failed');
  });
  // go perform the actual asynchronous saving
  _db.save();
  return promise;
};

In the example we created a new instance of a promise which means we need to create a promise class. The following will be an ES5 implementation, but I will include an ES6 implementation at the end.

Promise Class

var Promise = function() {};

We handle promises by passing success and failure handlers through via the then function. We can implement the then function as follows:

Promise.prototype.then = function(success, failure) {
  this.success = success;
  this.failure = failure;
};

The promise object also has a resolve and a reject method. Resolving a promise calls the success function we get from the then call. Rejecting calls the failure function (also received from the then call).

Promise.prototype.resolve = function(msg) {
  this.success(msg);
};

Promise.prototype.reject = function(msg) {
  this.failure(msg);
};

What we have right now is a very basic wrapper around callback functions. This works, but remember that the main advantage of promises is that they are chainable. We deal with asynchronous events linearly by chaining .then functions. Our current implementation does not return anything that has a then function. We need to augment it to return a new promise so we can start building a promise chain.

Promise.prototype.then = function(success, failure) {
  this._promise = new Promise();
  this.success = success;
  this.failure = failure;
  return this._promise;
};

We need to also modify our resolve and reject methods to call any functions that are registered further down the promise chain. We change our resolve and reject implementations to something like…

Promise.prototype.resolve = function(msg) {
  if (this.success) {
    this.success(msg);
  }
  if (this._promise) {
    this._promise.resolve(msg);
  }
};

Promise.prototype.reject = function(msg) {
  if (this.failure) {
    this.failure(msg);
  }
  if (this._promise) {
    this._promise.reject(msg);
  }
};

Most of the work is done, but there is still one thing we need to consider. When we call our success method, it itself might return a promise. The outer promise needs to handle this inner promise before proceeding with the next “then” block.

We go back to our uploader example:

...
button.addEventListener('click', function() {
  myUploader.upload('myfile')
    .then(function() {
      return database.save('new data to save');
    })
    .then(
      function() {
        alert('done saving');
      },
      function () {
        alert('error saving!');
      }
    );
});

If our upload succeeds, we trigger a database save. The database save returns a promise. This promise needs to be resolved before we know whether to trigger the alert “done saving” function or the alert “error saving” function. So once again we modify our resolve function.

Promise.prototype.resolve = function(msg) {
  // Note that the success function might return a promise
  // or it might just return a simple value.
  var mySuccessPromise;
  if (this.success) {
    mySuccessPromise = this.success(msg);
  }

  if (this._promise) {
    // We handle the case where the success call returns nothing
    if (!mySuccessPromise) {
      return this._promise.resolve();
    }

    // We handle the case where the success return is just a value
    if (!mySuccessPromise instanceof Promise) {
      return this._promise.resolve(mySuccessPromise);
    }

    // We handle the case where the success return is a promise
    return mySuccessPromise
      .then(this._promise.success, this._promise.failure);
  }
};

We now have a basic implementation of promises.


Wrapping Up

A good exercise to further understand promises is to implement some of the other helper methods included in many promise libraries such as “catch” and “all”.

Below is the code we wrote in it’s entirety.

ES5 Implementation

var Promise = function() {};

Promise.prototype.then = function(success, failure) {
  this._promise = new Promise();
  this.success = success;
  this.failure = failure;
  return this._promise;
};

Promise.prototype.resolve = function(msg) {
  var mySuccessPromise;
  if (this.success) {
    mySuccessPromise = this.success(msg);
  }

  if (this._promise) {
    if (!mySuccessPromise) {
      return this._promise.resolve();
    }

    if (!mySuccessPromise instanceof Promise) {
      return this._promise.resolve(mySuccessPromise);
    }

    return mySuccessPromise
      .then(this._promise.success, this._promise.failure);
  }
};

Promise.prototype.reject = function(msg) {
  if (this.failure) {
    this.failure(msg);
  }

  if (this._promise) {
    this._promise.reject(msg);
  }
};

ES6 Class

class Promise {

  then(success, failure) {
    this._promise = new Promise();
    this.success = success;
    this.failure = failure;
    return this._promise;
  }

  resolve(msg) {
    var mySuccessPromise;
    if (this.success) {
      mySuccessPromise = this.success(msg);
    }

    if (this._promise) {
      if (!mySuccessPromise) {
        return this._promise.resolve();
      }

      if (!mySuccessPromise instanceof Promise) {
        return this._promise.resolve(mySuccessPromise);
      }

      return mySuccessPromise
        .then(this._promise.success, this._promise.failure);
    }
  }

  reject(msg) {
    if (this.failure) {
      this.failure(msg);
    }

    if (this._promise) {
      this._promise.reject(msg);
    }
  }
}