Friday, February 5, 2021

Asynchronous Programming

A promise is an action that produces a result at some point in the future, unless it dies with an exception.
To combine two promises, you need to use method calls. Instead, the async/await constructs give you a simpler syntax that has the ordinary sequential flow.

1 JavaScript Concurrent Programming

What is a concurrent program?

  • a program is concurrent if it controls multiple activities which execute at same time, with overlapping timelines

What is I/O operations?

  • I/O (input/output) operations are also known as time-consuming operation.
  • I/O operaions occurr when the program interacts with the system’s disk and network.
  • I/O operations include reading/writing data from/to a disk, making HTTP requests and querying databases.

Concurrent programs in Java and C++

  • these programming languages are multithreaded
  • a program has a main thread, time-consuming operations are handled in a separate thread of execution
  • if multiple threads modify shared data, access to data must be regulated to prevent data corruption

Concurrent programs in JavaScript

  • JavaScript programs run in a single thread. If a function starts it will have to complete its execution, before another function is able to start
  • if a JavaScript program has to wait for something to happen, it cannot do something else in another thread
  • if a function modifies some shared data, other functions cannot access this data before the function returns.
    This means that in your javascript programs you don’t have to worry about synchronizations and mutex locks.

How JavaScript process time-consuming operations?

  • JavaScript has three features that allow you to run code in concurrently: callbacks, promises and async/await construct
  • time-consuming operations in JavaScript are asynchronous, also known as non-blocking
  • for example a program that is waiting for a HTTP response specifies a callback function; the current function continues execution and the callback is invoked when data is available

Example: a function that loads a JSON object from a given url

const loadJSON = url => {
  const request = new XMLHttpRequest();
  request.addEventListener('load', (e) => {
    if (request.status === 200) { // status code for success
      let json = JSON.parse(request.responseText);
      console.log({json})
    } else { 
      console.error(`Server responded with a ${request.status}`)
    }
  });
  request.addEventListener('error', (e) => console.error("NetworkError"));
  request.addEventListener('timeout', (e) => console.error("Timeout!!"));    
  request.open('GET', url, true);
  request.send();
};  

Note to the example:

  • the function uses the XMLHttpRequest API
  • the response's payload is processed sometime later in the asynchronous callback
  • an invocation to the loadJSON function returns immediately and does not wait for the data to load

Callbacks inside callbacks or Callbacks Hell

  • Doing tasks in order requires callbacks inside callbacks
  • Consider a situation where your program loads a page which contains the URL of an image and then it loads the image as well: this leads to nested calls

Promises VS Callbacks

  • promises allow to combine asynchronous tasks without nesting callbacks
  • promises make it easy to link completion and error actions than callbacks

2 Making Promises

How to build promises

  • you can make promises either by invoking APIs that yield promises or by invoking the Promise constructor
  • note: it is more likely that you use libraries that return promises, than you invoke the Promise constructor for making your own promises

Example: a fetch call yields a promise that resolves with a Response object

// fetch JSON for client IP address
let promise = fetch('https://api.ipify.org?format=jsonp&callback=?');
console.log(promise); // Promise {<pending>}

// fetch image file
promise = fetch('https://upload.wikimedia.org/wikipedia/commons/d/d9/Wikipedia_Monument_2.JPG');
console.log(promise); // Promise {<pending>}

Building promises invoking the Promise constructor, a summary of the process

const myPromise = new Promise((resolve, reject) => {
  const callback = (args) => {
    if (success) resolve(result) 
    else reject(reason)
  }
  asynTask(callback)
})
  • the Promise constructor takes as its argument a function, called executor function
  • the executor function takes two parameters, which are handler functions
  • In the body of the executor function you start the async task:
    • if the task yields a result, the result is passed to the resolve handler
    • if the task fails, the reject handler is invoked passing the reason of the failure

Example: the promiseSum promise factory function

// A factory that yields a promise that resolves with the sum two given numbers
function promiseSum(add1, add2, delay = 1000) {
  return new Promise((resolve, reject) => {
    const callback = () => {
      let sum = add1+add2;
      resolve(sum);
    }
    setTimeout(callback, delay);
  })
}

Note to the example

  • the executor function invokes setTimeout passing a callback and a delay.
    After the delay has passed, the callback is invoked, which passes the result to the resolve handler

Promise Control Flow

  • the life cycle of a js promise: from the promise's creation to the promise's result production
  • The Promise() constructor is called.
    • the JavaScript interpreter invokes the constructor argument: the executor function is called, being passed resolve and reject
    • the executor function set up the asynchronous task and returns
    • the constructor returns.
    • at this point, the promise is in the pending state.
  • some time passes ...
  • the async task completes and invokes a callback that you provided in the executor function
    • if in that callback you have a call to reject handler, the promise is rejected and settled
    • if in the callback you have a call to resolve handler with an object other than a promise, then the promise is fulfilled and settled
    • if in the callback you have a call to resolve with another promise, then the current promise is resolved but not fulfilled

3 Handling promises using methods

The Promise.prototype.then(onFulfilled) method allows to define a fulfillment handler

promise.then(value => {…})
  • the fulfillment handler processes the result of the promise
  • the fulfillment handler is to be executed when the promise is resolved

Example: get the result of the delayed sum of two numbers

function promiseSum(add1, add2, delay = 1000) {
  return new Promise((resolve, reject) => {
    const callback = () => {
      let sum = add1+add2;
      resolve(sum); 
    }
    setTimeout(callback, delay);
  })
}
let promise = promiseSum(25, 25);
console.log(promise);  // Promise {<pending>}
// in then method, get the sum, result of the promise
promise.then((value) => {
  console.log(promise); // Promise { 50 }
  console.log("Sum is: ", value); // Sum is: 50
});

Example: get the Response, result of fetch invocation

// fetch makes a promise that resolves with a Response object
let promise = fetch('https://api.ipify.org?format=json');
console.log(promise); // Promise {<pending>}
// in then method, get Request object, result of fetch promise
promise.then(response => {
  console.log(promise); // Promise {<fulfilled>: Response}
  console.log(response) ; // Response {type: "cors", url: "https://api.ipify.org/?format=json", redirected: false, status: 200, ok: true, …}
});

The Promise.prototype.then method allows to chain promises

  • As already said, ECMAScript 6 introduced promises to make it easier to do one asynchronous task and then another:
    you can use the then method to pass the result of a promise to another promise
  • after a Promise is fulfilled, the then method is called; suppose that the then's handler produces another promise:
    to process the result of the second promise, invoke the then method once again

Example: get the result of response.json() by invoking then method

// fetch() returns a promise and response.json() returns a promise as well 
fetch('https://api.ipify.org?format=json')
  .then( response => {  // first then invocation
    console.log(response) ; // Response { … }
    return response.json();
  })
  .then( json => {  // append a second then invocation
    console.log(json); // {ip: "83.47.216.44"}
    document.getElementById('text').textContent = `IPv4: ${json.ip}`;
  })

The Promise.prototype.catch(onRejected) method allows to define a rejection handler

promise.catch(reason => {…})
  • the rejection handler is called if a rejection has happened.
  • the catch method yields a new promise, the same way the then method yields a new promise.
  • the result of the new promise depends on the return value of the rejection handler:
    • if it is a value, the promise is now fulfilled with that value;
      if it throws another exception, the promise is still rejected with a given reason;
      if it returns another promise, that promise is chained
    • commonly, the rejection handler deals with the problem and then returns a value to resolve the promise

The Promise.prototype.finally(onFinally) method invokes the onFinally handler after the promise is settled

promise.finally(() => {…})
  • the finally handler is always invoked, no matter the promise is resolved or rejected
  • the finally handler has the purpose of doing some clean up that has to occur no matter what happend before
  • the handler has no arguments

Example: use catch and finally methods with fetch API

const update = () => {
  // get JSON for client IP
  fetch('https://api.ipify.org?format=json')  
    .then(response => response.json())
    .then(json => {
      // update DOM to show IP
      document.querySelector('section#result').innerHTML=
       `<h2>My Public IP</h2>              
        <p class="ip"> ${json.ip} </p>
        <p> Last update: ${(new Date()).toLocaleTimeString()}</p>`;
    })
    .finally(() => {
      // always update DOM to add the reload button
      document.querySelector('section#reload').innerHTML = 
        `<a id="refresh-icon" href="#" title="refresh"><i class="fas fa-sync-alt"></i></a>`
      document.getElementById('refresh-icon').addEventListener('click', e => {
        e.preventDefault();
        update();
      });
    })
    .catch(err => {
      // update DOM with error message
      console.error(err);
      document.querySelector('section#result').textContent='Sorry, we cannot load data'; 
    })
}

document.addEventListener('DOMContentLoaded', update);

See the Pen fetch then finally catch by Massimiliano De Simone (@maxdesimone) on CodePen.

4 Handling promises with async/await construct

Which programming style to use with promises?

  • the asynchronous programming style
    • a chain of then/finally/catch method invocations
    • code hard to read and reason about
  • the synchronous programming style
    • await operator within async functions
    • code with sequence of statements and using the traditional control flow

The await operator

let value = await promise
  • the value of the await operator is the fulfillment value of the promise object
  • it makes your program appear as it were waiting for the promise to settle
  • it can only occur within a function tagged by async keyword

Example: async function with await fetch

const loadJSON = async (url) => {
  // await gets fetch promise result
  const response = await fetch(url);
  // await gets response.json promise result
  const json = await response.json();
}

Async functions

  • the JavaScript interpreter rewrites async functions: code after await is put into a then handler and executed after the promise resolves
  • You can apply the async keyword to different kinds of functions: function expressions and function definitions, arrow functions, class methods and object literal methods
  • the result of the async keyword is a function object which is an instance of the AsyncFunction class

Example: async functions sequential programming style

const update = async () => {
  try {
    // remove refresh and add spinner icon
    document.querySelector('section#spinner').innerHTML =  
      `<a id="refresh-icon" href="#" title="refresh">
        <i class="fas fa-sync-alt fa-spin"></i></a>`;  
    // await fetch() evaluates to http response
    const response = await fetch('https://api.ipify.org?format=json');
    // await response.json() evaluates to json  
    const json = await response.json();
    // show result to user
    document.querySelector('section#result').innerHTML=
       `<h2> My Public IP </h2>              
        <p class="ip"> ${json.ip} </p>
        <p> Last update: ${(new Date()).toLocaleTimeString()} </p>`;    
  } catch (err) {
    // show error message to user
    document.querySelector('section#result').textContent='Sorry, we cannot load data'; 
    console.error(err);
  } finally {
    // always remove spinner & add refresh icon
    document.querySelector('section#spinner').innerHTML = 
      `<a id="refresh-icon" href="#" title="refresh">
        <i class="fas fa-sync-alt"></i></a>`;
    document.getElementById('refresh-icon').addEventListener('click', e => {
      e.preventDefault();
      update();
    });
  } // END finally
} // END try

document.addEventListener('DOMContentLoaded', update);

See the Pen async function by Massimiliano De Simone (@maxdesimone) on CodePen.

Async functions return values

  • async functions always return a promise
  • if the function returns a value, the value becomes a promise
  • if the function returns null, it actually returns Promise.resolve(null)

Example: an async function returning a value

// the function returns a string value, but the value becomes a promise
const getClientIP = async () => {
  const response = await fetch('https://api.ipify.org?format=json');
  const json = await response.json();
  return json.ip; // returns a promise
}
// To get a value returned by async functions invocations:
// - either invoke the async function and chain a then method
getClientIP().then( value => console.log(value) )
// - or invoke the async function from within another async function
const clientFunction = async () => {
  const value = await getClientIP();
}
clientFunction();

Example: async function return null

const loadJSON = async (url) => {
  // return null becomes Promise.resolve(null)
  if (url === undefined) return null; 
  // await gets fetch promise result
  const response = await fetch(url);
  // await gets response.json promise result
  const json = await response.json();
}

What if an async function throws an exception?

  • if a statement of an async function throws an exception, the async function yields a rejected promise

What if an await get a rejected promise?

  • if await operator gets a rejected promise, it throws an exception

Handling errors by async function invocations

  • when using await, you should set up a strategy for error handling
  • for example, the top level async function encloses all async function invocations within a try/catch statement

Example: async function that does exception handling

See the Pen async functions exception handling by Massimiliano De Simone (@maxdesimone) on CodePen.

No comments:

Post a Comment