- The promise can only be resolved once--be this keeping or breaking it
- An optional callback for failure can be supplied with a success callback
- Multiple callbacks can be supplied and they will be executed in the same order when resolving the promise
With everything laid out, I started my timer and began coding. It didn’t take long to get something up and running that allowed a single set of success and failure callbacks to be queued but the requirement to be able to supply multiple callbacks was a bit more of a challenge. It took a little bit of a design change and some refactoring before I ended up with a solution that I was happy with. This is what I came up with:
(function (promiseApi) { promiseApi.Promise = function () { var isResolved = false; var successCallbacks = []; var failureCallbacks = []; var keep = function (data) { resolve(successCallbacks, data); }; var abandon = function (error) { // "break" is a keyword unfortunately resolve(failureCallbacks, error); }; var resolve = function (queue, state) { if (!isResolved) { isResolved = true; queue.forEach(function (callback) { state = callback(state); }); successCallbacks = []; failureCallbacks = []; } }; var when = function (success, failure) { if (success !== undefined) { successCallbacks.push(success); } if (failure !== undefined) { failureCallbacks.push(failure); } return Object.freeze({ when : when }); }; return Object.freeze({ keep: keep, abandon: abandon, promise: Object.freeze({ when: when }) }); }; }(module.exports));
Here are some notes about my solution:
- I decided to include an “isResolved” Boolean even though I am emptying the arrays as a guard against the promise being resolved again before all the callbacks in the appropriate queue have been invoked. This ensures that any additional calls to resolve the promise will result in no further action.
- The arrays are being re-initialized as a housekeeping measure to release any resources that they are consuming. As a promise can only be resolved once, it doesn’t make sense to hold on to any of the callbacks in its queues. Originally, I was setting the length of the array to 0, but I learned that this actually blocks the garbage collector until the items in the array are overwritten by new items. In normal use of a promise, you wouldn't add additional success and failure callbacks after the promise had been resolved, so the original callbacks would remain in memory.
- The use of the “setTimeout” function with a timeout of 0 is to ensure that the callbacks are invoked immediately. However, as this is a node.js project it probably would be more efficient to use process.nextTick().
- To prevent the behavior of a promise being changed, I have used Object.freeze() to make it immutable.
- In an attempt to keep things clean, I used the revealing module pattern.
This is a very basic implementation of a promise API. I’m sure that there is much that doesn’t adhere to the promise specification, but as I mentioned above, this was just a challenge that I set myself to exercise my JavaScript muscles. I wouldn’t recommend that anyone use this code in production even though—technically—it does work. For one, there’s no error handling and if anything were to go wrong when calling a callback, it would continue to bubble up the call stack. The start of a solution would be to wrap the callback’s invocation in a try-catch block, but what would the appropriate course of action be once an exception has been caught? I think I’d have to refer to the specification for guidance on that. Below is a simple example of how to use the promise API:
carService.js:
(function (carService) { var promiseApi = require('./promise.js'); carService.get = function (id) { var promise = new promiseApi.Promise(); setTimeout(function () { var data = { id: id, model: 'Ford Mustang ' + id, year: 2014 }; promise.keep(data); //promise.abandon({ message: "Something went wrong" }); }, 1000); return promise.promise; }; }(module.exports));
app.js:
(function () { var carService = require('./fakeCarService.js'); carService.get(1) .when(function (data) { console.log(data.model); return data.model; }, function (error) { console.log("Error: " + error.message); return error; }) .when(function (data) { console.log("Second CB: " + data); return data; }) .when(function (data) { console.log("Third CB: " + data); }, function (error) { console.log("Second Error CB: " + error.message); }); }());
As a challenge to help me practice JavaScript, this was a good problem to solve. The complete node.js project can be found here.