Promises

ES6 introduces promises as an alternate to callbacks. Like callbacks, promises are used to retrieve the results of an asynchronous function call. Using promises is easier than callbacks and produces more readable code. However, to implement promises for your asynchronous functions requires more work.

A promise object represents a value that may be available now or in the future, or possibly never. As the name suggests, a promise may be fulfilled or rejected. A promise acts as a placeholder for the eventual result.

A promise has three mutually exclusive states, which are as follows:

  1. A promise is pending before the result is ready; this is the initial state.
  2. A promise is fulfilled when the result is ready.
  3. On an error, a promise is rejected.

When a pending promise is either fulfilled or rejected, associated callbacks/handlers that are queued up by the then() method of the promise are executed.

The purpose of promises is to provide a better syntax for the CPS callbacks. The typical CPS style asynchronous functions like the following one:

    asyncFunction(arg, result => { 
      //... 
    }) 

The preceding code can be written a bit differently with promises, as shown in the following lines of code:

    asyncFunction(arg). 
    then(result=>{ 
      //... 
    }); 

The asynchronous function now returns a promise, which is the placeholder for an eventual result. Callbacks registered with the then() method are notified when the result is ready.

You can chain the then() method. When the then() method sees that the callback triggered another asynchronous action that returns a promise, it returns that promise. Take a look at the following example:

    asyncFunction(arg) 
    .then(resultA=>{ 
      //... 
      return asyncFunctionB(argB); 
    }) 
    .then(resultB=>{ 
      //... 
    }) 

Let's see a real example of how we can use promises. We saw a typical example of asynchronous file reads in Node.js; now let's see what that example will look like when used with promises. To jog our memories, we wrote something like the following:

    fs.readFile('text.json', 
      function (error, text) { 
          if (error) { 
              console.error('Error while reading text file'), 
          } else { 
              try { 
                  //... 
              } catch (e) { 
                  console.error('Invalid content'), 
              } 
          } 
      }); 

We see callbacks as continuation here; now let's see how the same function can be written using promises:

    readFileWithPromises('text.json') 
    .then(text=>{ 
      //...process text 
    }) 
    .catch(error=>{ 
      console.error('Error while reading text file'), 
    }) 

Now the callbacks are invoked via the result and methods then() and catch(). The error handling is much cleaner because we are not writing the if...else and try...catch constructs anymore.

Creating promises

We saw how we can consume promises. Now, let's look at how we can produce them.

As a producer, you can create a Promise object and send a result via the Promise. The construct looks like the following code snippet:

    const p = new Promise( 
      function (resolve, reject) { // (1) 
          
          if (   ) { 
              resolve(value); // success 
          } else { 
              reject(reason); // failure 
          } 
      }); 

The parameter to Promise is an executor function. The executor handles two states of the promise, which are as follows:

  • Resolving: If the result was generated successfully, the executor sends the results back via the resolve() method. This method usually fulfills the Promise object.
  • Rejecting: If an error happened, the executor notifies the consumer via the reject() method. If an exception occurs, it is notified via the reject() method as well.

As a consumer, you are notified of either fulfillment of promise or rejection of promise via the then() and catch() methods. Consider the following piece of code as an example:

    promise 
    .then(result => { /* promise fulfilled */ }) 
    .catch(error => { /* promise rejected */ }); 

Now that we have some background on how to produce promises, let's rewrite our earlier example of the asynchronous file's read method to produce promises. We will use Node.js's filesystem module and the readFile() method as we did last time. If you don't understand any Node.js specific construct in the following snippet, please don't worry. Consider the following code:

    import {readFile} from 'fs'; 
    function readFileWithPromises(filename) { 
        return new Promise( 
            function (resolve, reject) { 
                readFile(filename,  
                    (error, data) => { 
                        if (error) { 
                            reject(error); 
                        } else { 
                            resolve(data); 
                        } 
                    }); 
            }); 
    } 

In the preceding snippet, we are creating a new Promise object and returning it to the consumer. As we saw earlier, the parameter to the Promise object is the executor function and the executor function takes care of two states of Promise-fulfilled and rejected. The executor function takes in two arguments, resolve and reject. These are the functions that notify the state of the Promise object to the consumer.

Inside the executor function, we will call the actual function-the readFile() method; if this function is successful, we will return the result using the resolve() method and if there is an error, we will notify the consumer using the reject() method.

If an error happens in one of the then() reactions, they are caught in the subsequent catch() block. Take a look at the following code:

    readFileWithPromises('file.txt') 
    .then(result=> { 'something causes an exception'}) 
    .catch(error=> {'Something went wrong'}); 

In this case, the then() reaction causes an exception or error, and the subsequent catch() block can handle this.

Similarly, an exception thrown inside a then() or catch() handler is passed to the next error handler. Consider the following code snippet:

    readFileWithPromises('file.txt') 
    .then(throw new Error()) 
    .catch(error=> {'Something went wrong'}); 

Promise.all()

One interesting use case is to create an iterable over promises. Let's assume that you have a list of URLs you want to visit and parse the results. You can create promises for each of the fetch URL calls and use them individually, or you can create an iterator with all the URLs and use the promise in one go. The Promise.all() method takes the iterable of promises as an argument. When all of the promises are fulfilled, an array is filled with their results. Consider the following code as an example:

    Promise.all([ 
        f1(), 
        f2() 
    ]) 
    .then(([r1,r2]) => { 
        //    
    }) 
    .catch(err => { 
        //.. 
    }); 

Metaprogramming and proxies

Metaprogramming refers to a method of programming where the program is aware of its structure and can manipulate itself. Many languages have support for metaprogramming in the form of macros. Macros are important constructs in functional languages such as LISP (Locator/ID Separation Protocol). In languages such as Java and C#, reflection is a form of metaprogramming because a program can examine information about itself using reflection.

In JavaScript, you can say that methods of object allow you to examine the structure and hence, they offer metaprogramming. There are three types of metaprogramming paradigms (The Art of the Metaobject Protocol, Kiczales et al, https://mitpress.mit.edu/books/art-metaobject-protocol):

  • Introspection: This gives a read-only access to the internals of a program
  • Self-modification: This makes structural changes possible to the program
  • Intercession: This changes language semantics

The Object.keys() method is an example of introspection. In the following example, the program is examining its own structure:

    const introspection = { 
      intro() { 
        console.log("I think therefore I am"); 
      } 
    } 
    for (const key of Object.keys(introspection)){ 
      console.log(key);  //intro 
    } 

Self-modification is also possible in JavaScript by mutating the properties of an object.

However, intercession, or the ability to change language semantics, is something not available in JavaScript till ES6. Proxies are introduced to open up this possibility.

Proxy

You can use a proxy to determine the behavior of an object, which is called the target, whenever its properties are accessed. A proxy is used to define custom behavior for basic operations on an object, such as looking up a property, function invocation, and assignment.

A proxy needs two parameters, which are as follows:

  • Handler: For each operation you want to customize, you need a handler method. This method intercepts the operations and is sometimes called a trap.
  • Target: When the handler does not intercept the operation, the target is used as a fallback.

Let's take a look at the following example to understand this concept better:

    var handler = { 
      get: function(target, name){ 
        return name in target ? target[name] :42; 
      } 
    } 
    var p = new Proxy({}, handler); 
    p.a = 100; 
    p.b = undefined; 
    console.log(p.a, p.b); // 100, undefined 
    console.log('c' in p, p.c); // false, 42 

In this example, we are trapping the operation of getting a property from the object. We return 42 as a default property value if the property does not exist. We are using the get handler to trap this operation.

You can use proxies to validate values before setting them on an object. For this, we can trap the set handler as follows:

    let ageValidator = { 
      set: function(obj, prop, value) { 
        if (prop === 'age') { 
          if (!Number.isInteger(value)) { 
            throw new TypeError('The age is not an number'), 
          } 
          if (value > 100) { 
            throw new RangeError('You cant be older than 100'), 
          } 
        } 
        // If no error - just store the value in the property 
        obj[prop] = value; 
      } 
    }; 
    let p = new Proxy({}, ageValidator); 
    p.age = 100; 
    console.log(p.age); // 100 
    p.age = 'Two'; // Exception 
    p.age = 300; // Exception 

In the preceding example, we are trapping the set handler. When we set a property of the object, we are trapping that operation and introducing validation of values. If the value is valid, we will set the property.

Function traps

There are two operations that can be trapped if the target is a function: apply and construct.

To intercept function calls, you will need to trap the get and apply operations. First get the function and then apply to call the function. So, you get the function and return the function.

Let's consider the following example to understand how method interception works:

    var car = { 
      name: "Ford", 
      method_1: function(text){ 
        console.log("Method_1 called with "+ text); 
      } 
    } 
    var methodInterceptorProxy = new Proxy(car, { 
     //target is the object being proxied, receiver is the proxy 
     get: function(target, propKey, receiver){ 
      //I only want to intercept method calls, not property access 
      var propValue = target[propKey]; 
      if (typeof propValue != "function"){ 
       return propValue; 
  } 
      else{ 
       return function(){ 
        console.log("intercepting call to " + propKey
          + " in car " + target.name); 
        //target is the object being proxied 
        return propValue.apply(target, arguments); 
       } 
      } 
     } 
    }); 
    methodInterceptorProxy.method_1("Mercedes"); 
    //"intercepting call to method_1 in car Ford" 
    //"Method_1 called with Mercedes" 

In the preceding example, we are trapping the get operation. If the type of the property being get is a function, we will use apply to invoke that function. If you see the output, we are getting two console.logs; the first is from the proxy where we trapped the get operation and the second is from the actual method call.

Metaprogramming is an interesting construct to use. However, any kind of introspection or reflection comes at the cost of performance. Care should be taken while using proxies as they can be slow.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.217.109.151