Asynchronous JavaScript

Parallelism in JavaScript

JavaScript engines are single-threaded:

  • All code runs in a single thread, in a single process,

    • In old browsers: one process for the whole browser,
    • In modern browsers: one tab = one process.
  • No native way to do background work.

  • All asynchronism is implemented through an event loop:

    while True:                   # Fictive implementation of an event loop
        while queue.is_empty():
            wait()
        task = queue.pop()
        task.execute()
    
  • Two types of function calls:

    • Blocking: they execute immediately, block the thread until they are done,
    • Non-blocking: they yield control to the event loop, queue a task when they are done.

Function calls: blocking vs non-blocking

Blocking calls

Most function calls:

  • User defined functions,
  • Computations: Math, string manipulations, array/object manipulations, …
  • DOM API: .querySelector(), .appendChild, …
  • eval, require, console.log, …

Non-blocking calls

  • Sleeping: setTimeout(), setInterval(),
  • Input/Output:
    • System: communicating with other processes (e.g., shell commands), …
    • Disk: reading/writing files, reading/executing templates, …
    • Network: reading/writing on sockets, HTTP requests/responses, XMLHttpRequest, fetch(), app.listen(), …
    • Databases: connecting to databases, querying databases, knex.raw(), knex.select(), …

Handling asynchronous calls: callbacks

Classic technique, typical of event-based programming

function callback() {             // (click to run)
  alert('Hello');
}
setTimeout(callback, 2000);       // Run callback in 2 secs
alert('world');
  1. setTimeout queues callback, to be executed in 2 seconds,
  2. Control goes to the next instruction,
  3. After 2 seconds, callback is executed.

Also seen in event handlers, request handlers:

// Call when a click happens
div.addEventListener('click',
  function(e) {
    console.log('Hello click');
  });
// Call when a HTTP request happens
app.get('/toto',
  function(req, res) {
    res.send("Hello world");
  });

The problem with callbacks: Callback hell

app.get('/foo', function(req, res) {
  setTimeout(function() {
    knex.raw('SELECT * FROM users').asCallback(function(err, result) {
      if (err)
        console.log(err);
      else if (result.length > 0) {
        knex.raw(...).asCallback(...)
      }
    });
  }, 2000);
});

This style is error prone:

  • Code flow hard to follow,
  • Errors difficult to catch,
  • Very painful to write loops:
    • exercise: write “Hello world” to the console every 2 seconds, without using setInterval.
  • Hard to synchronize:
    • waiting for the end of two or more actions,
    • waiting for the end of the first among two or more actions.

Promises and async/await

Modern API and new syntax (only since ES2017) to handle asynchronous code

async function get_readme() {
  try {
    var response = await fetch('/README.md');    // wait for HTTP response
    var content = await response.text();         // wait for end of response body
    alert(content);
  } catch (err) {                                // asynchronous errors are catched too
    console.error(err);
  }
}
get_readme();
  1. Non-blocking functions (e.g., fecth()) return Promises,
  2. await blocks execution until the promise is done,
  3. when the promise is done await passes on the promised value.
  • Must always be wrapped in async function.
  • Can be used in for loops, etc.
  • Only supported by modern APIs and libraries, e.g., fetch(), Knex, …

Memorandum: long story short

  • Always know which calls are non-blocking:
    • Local input/output;
    • Network requests (e.g., fetch());
    • Database queries (e.g., knex.raw(), knex.from(), …);
  • Always use await in front of non-blocking calls.

  • Always wrap await in async function.

  • Don’t forget to catch errors with try ... catch.

Sadly: most failures to do so will result in subtle, not-so-easy-to-read, errors!

Ok. Now to the hardcore stuff!

What on earth is a Promise?!

async/await is just syntactic sugar for:

var p1 = fetch('/README.md');
var p2 = p1.then(function(response) {
    return response.text();
});
var p3 = p2.then(function(content) {
    console.log(content);
});
var p4 = p3.catch(function(err) {
    console.error(err);
});
console.log(p1, p2, p3, p4);
  • p1, p2, p3, p4 are all Promises;
  • A promise is a sort of box protecting a value;
  • You (the programmer) are only allowed to act on the value by passing callbacks.

Promises are boxes

p1 = ? http://... .then( ) function(response) { return response.text()} p2 = ? AWS – ... .then( ) function(content) { console.log(content)} p3 = ? undefined .catch( ) function(err) { console.error(err)} p4 = ? undefined

Chain promises

This style is more succint, and preferred:

fetch('/README.md')
    .then(function(response) {
        return response.text();
    })
    .then(function(content) {
        alert(content);
    })
    .catch(function(err) {
        console.error(err);
    });

Good to know:

  • JavaScript automatically decapsulates nested promises (e.g., response.text() returns a promise, the boxed value is passed to the next callback).
  • Errors in the callbacks are passed to .catch().
  • An error not handled by a .catch() will usually crash the application.

Careful with naked promises

Racing promises

var start = Date.now()
function cb(response) {
  var elapsed = Date.now() - start;
  alert(`${response.url} (${elapsed}ms)`);
}
fetch('/index.html').then(cb);             // these two execute in parallel
fetch('/README.md').then(cb);

If you want to order requests, you must chain callbacks (or use async/await)

var start = Date.now()
fetch('/index.html')
  .then(function (response) {
    alert(`${response.url} (${Date.now() - start}ms)`);
    return fetch('/README.md');           // fetch 2nd only after 1st is done
  })
  .then(function (response) {
    alert(`${response.url} (${Date.now() - start}ms)`);
  });

Advanced parallelism

Promise.all([                         // these two execute in parallel
  fetch('/index.html'),
  fetch('/README.md'),
]).then(function(responses) {
  alert(responses[0].url + ", "
        + responses[1].url);
  return fetch('/');                  // this one executes after
}).then(function(response) {
  alert(response.url);
});
  • Create your own promises using new Promise(),
  • Advanced parallelism using the Promise inferface,
  • Read more on MDN.

References

Fork me on GitHub