Inheriting from Promises

Just like other built-in types, you can use a promise as the base for a derived class. This allows you to define your own variation of promises to extend what built-in promises can do. Suppose, for instance, you’d like to create a promise that can use methods named success() and failure() in addition to the usual then() and catch() methods. You could create that promise type as follows:

  1. class MyPromise extends Promise {
  2. // use default constructor
  3. success(resolve, reject) {
  4. return this.then(resolve, reject);
  5. }
  6. failure(reject) {
  7. return this.catch(reject);
  8. }
  9. }
  10. let promise = new MyPromise(function(resolve, reject) {
  11. resolve(42);
  12. });
  13. promise.success(function(value) {
  14. console.log(value); // 42
  15. }).failure(function(value) {
  16. console.log(value);
  17. });

In this example, MyPromise is derived from Promise and has two additional methods. The success() method mimics then() and failure() mimics the catch() method.

Each added method uses this to call the method it mimics. The derived promise functions the same as a built-in promise, except now you can call success() and failure() if you want.

Since static methods are inherited, the MyPromise.resolve() method, the MyPromise.reject() method, the MyPromise.race() method, and the MyPromise.all() method are also present on derived promises. The last two methods behave the same as the built-in methods, but the first two are slightly different.

Both MyPromise.resolve() and MyPromise.reject() will return an instance of MyPromise regardless of the value passed because those methods use the Symbol.species property (covered under in Chapter 9) to determine the type of promise to return. If a built-in promise is passed to either method, the promise will be resolved or rejected, and the method will return a new MyPromise so you can assign fulfillment and rejection handlers. For example:

  1. let p1 = new Promise(function(resolve, reject) {
  2. resolve(42);
  3. });
  4. let p2 = MyPromise.resolve(p1);
  5. p2.success(function(value) {
  6. console.log(value); // 42
  7. });
  8. console.log(p2 instanceof MyPromise); // true

Here, p1 is a built-in promise that is passed to the MyPromise.resolve() method. The result, p2, is an instance of MyPromise where the resolved value from p1 is passed into the fulfillment handler.

If an instance of MyPromise is passed to the MyPromise.resolve() or MyPromise.reject() methods, it will just be returned directly without being resolved. In all other ways these two methods behave the same as Promise.resolve() and Promise.reject().

Asynchronous Task Running

In Chapter 8, I introduced generators and showed you how you can use them for asynchronous task running, like this:

  1. let fs = require("fs");
  2. function run(taskDef) {
  3. // create the iterator, make available elsewhere
  4. let task = taskDef();
  5. // start the task
  6. let result = task.next();
  7. // recursive function to keep calling next()
  8. function step() {
  9. // if there's more to do
  10. if (!result.done) {
  11. if (typeof result.value === "function") {
  12. result.value(function(err, data) {
  13. if (err) {
  14. result = task.throw(err);
  15. return;
  16. }
  17. result = task.next(data);
  18. step();
  19. });
  20. } else {
  21. result = task.next(result.value);
  22. step();
  23. }
  24. }
  25. }
  26. // start the process
  27. step();
  28. }
  29. // Define a function to use with the task runner
  30. function readFile(filename) {
  31. return function(callback) {
  32. fs.readFile(filename, callback);
  33. };
  34. }
  35. // Run a task
  36. run(function*() {
  37. let contents = yield readFile("config.json");
  38. doSomethingWith(contents);
  39. console.log("Done");
  40. });

There are some pain points to this implementation. First, wrapping every function in a function that returns a function is a bit confusing (even this sentence was confusing). Second, there is no way to distinguish between a function return value intended as a callback for the task runner and a return value that isn’t a callback.

With promises, you can greatly simplify and generalize this process by ensuring that each asynchronous operation returns a promise. That common interface means you can greatly simplify asynchronous code. Here’s one way you could simplify that task runner:

  1. let fs = require("fs");
  2. function run(taskDef) {
  3. // create the iterator
  4. let task = taskDef();
  5. // start the task
  6. let result = task.next();
  7. // recursive function to iterate through
  8. (function step() {
  9. // if there's more to do
  10. if (!result.done) {
  11. // resolve to a promise to make it easy
  12. let promise = Promise.resolve(result.value);
  13. promise.then(function(value) {
  14. result = task.next(value);
  15. step();
  16. }).catch(function(error) {
  17. result = task.throw(error);
  18. step();
  19. });
  20. }
  21. }());
  22. }
  23. // Define a function to use with the task runner
  24. function readFile(filename) {
  25. return new Promise(function(resolve, reject) {
  26. fs.readFile(filename, function(err, contents) {
  27. if (err) {
  28. reject(err);
  29. } else {
  30. resolve(contents);
  31. }
  32. });
  33. });
  34. }
  35. // Run a task
  36. run(function*() {
  37. let contents = yield readFile("config.json");
  38. doSomethingWith(contents);
  39. console.log("Done");
  40. });

In this version of the code, a generic run() function executes a generator to create an iterator. It calls task.next() to start the task and recursively calls step() until the iterator is complete.

Inside the step() function, if there’s more work to do, then result.done is false. At that point, result.value should be a promise, but Promise.resolve() is called just in case the function in question didn’t return a promise. (Remember, Promise.resolve() just passes through any promise passed in and wraps any non-promise in a promise.) Then, a fulfillment handler is added that retrieves the promise value and passes the value back to the iterator. After that, result is assigned to the next yield result before the step() function calls itself.

A rejection handler stores any rejection results in an error object. The task.throw() method passes that error object back into the iterator, and if an error is caught in the task, result is assigned to the next yield result. Finally, step() is called inside catch() to continue.

This run() function can run any generator that uses yield to achieve asynchronous code without exposing promises (or callbacks) to the developer. In fact, since the return value of the function call is always coverted into a promise, the function can even return something other than a promise. That means both synchronous and asynchronous methods work correctly when called using yield, and you never have to check that the return value is a promise.

The only concern is ensuring that asynchronous functions like readFile() return a promise that correctly identifies its state. For Node.js built-in methods, that means you’ll have to convert those methods to return promises instead of using callbacks.

A> ### Future Asynchronous Task Running A> A> At the time of my writing, there is ongoing work around bringing a simpler syntax to asynchronous task running in JavaScript. Work is progressing on an await syntax that would closely mirror the promise-based example in the preceding section. The basic idea is to use a function marked with async instead of a generator and use await instead of yield when calling a function, such as: A> A> js A> (async function() { A> let contents = await readFile("config.json"); A> doSomethingWith(contents); A> console.log("Done"); A> })(); A> A> A> The async keyword before function indicates that the function is meant to run in an asynchronous manner. The await keyword signals that the function call to readFile("config.json") should return a promise, and if it doesn’t, the response should be wrapped in a promise. Just as with the implementation of run() in the preceding section, await will throw an error if the promise is rejected and otherwise return the value from the promise. The end result is that you get to write asynchronous code as if it were synchronous without the overhead of managing an iterator-based state machine. A> A> The await syntax is expected to be finalized in ECMAScript 2017 (ECMAScript 8).