Event Loop

  • it explains how Node.js can be asynchronous and have non-blocking I/O
  • In general, in most browsers there is an event loop for every browser tab, to make every process isolated and avoid a web page with infinite loops or heavy processing to block your entire browser.
  • The event loop continuously checks the call stack to see if there's any function that needs to run.

Blocking the event loop

  • Any JavaScript code that takes too long to return back control to the event loop will block the execution of any JavaScript code in the page, even block the UI thread, and the user cannot click around, scroll the page, and so on.
  • Almost all the I/O primitives in JavaScript are non-blocking. Network requests, filesystem operations, and so on. Being blocking is the exception, and this is why JavaScript is based so much on callbacks, and more recently on promises and async/await.

The Message Queue

  • When setTimeout() is called, the Browser or Node.js starts the timer. Once the timer expires, the callback function is put in the Message Queue.
  • The Message Queue is also where user-initiated events like click or keyboard events, or fetch responses are queued before your code has the opportunity to react to them. Or also DOM events
  • The loop gives priority to the call stack, and it first processes everything it finds in the call stack, and once there's nothing in there, it goes to pick up things in the message queue.

ES6 Job Queue

  • ECMAScript 2015 introduced the concept of the Job Queue, which is used by Promises (also introduced in ES6/ES2015). It's a way to execute the result of an async function as soon as possible, rather than being put at the end of the call stack.
  • Promises that resolve before the current function ends will be executed right after the current function.

    const bar = () => console.log('bar')

    const baz = () => console.log('baz')

    const foo = () => {
        console.log('foo')                                        // 1
        setTimeout(bar, 0)                                        // 4
        new Promise((resolve, reject) =>
            resolve('should be right after baz, before bar')
        ).then(resolve => console.log(resolve))                   // 3
        baz()                                                     // 2
    }

    foo()
        

Next Tick

  • Every time the event loop takes a full trip, we call it a tick.
  • When we pass a function to process.nextTick(), we instruct the engine to invoke this function at the end of the current operation, before the next event loop tick starts
  • It's the way we can tell the JS engine to process a function asynchronously (after the current function), but as soon as possible, not queue it
  • Calling setTimeout(() => {}, 0) will execute the function at the end of next tick, much later than when using nextTick() which prioritizes the call and executes it just before the beginning of the next tick.
  • Use nextTick() when you want to make sure that in the next event loop iteration that code is already executed

    process.nextTick(() => {
        //do something
    });
        

Set Immediate

  • A function passed to process.nextTick() is going to be executed on the current iteration of the event loop, after the current operation ends. This means it will always execute before setTimeout and setImmediate.
  • A setTimeout() callback with a 0ms delay is very similar to setImmediate(). The execution order will depend on various factors, but they will be both run in the next iteration of the event loop.

    setImmediate(() => {
        //run something
    })
        

Set Interval

  • setInterval is a function similar to setTimeout, with a difference: instead of running the callback function once, it will run it forever, at the specific time interval you specify (in milliseconds)

    setInterval(() => {
        // runs every 2 seconds
    }, 2000)
        

Callbacks

  • JavaScript is synchronous by default and is single threaded. This means that code cannot create new threads and run in parallel.
  • The browser provides a way to respond to user actions (eg mouse click) by providing a set of APIs that can handle this kind of functionality.

A callback is a simple function that's passed as a value to another function, and will only be executed when the event happens. We can do this because JavaScript has first-class functions, which can be assigned to variables and passed around to other functions (called higher-order functions)

    document.getElementById('button').addEventListener('click', () => {
        //item clicked
    });
        
every callback adds a level of nesting, and when you have lots of callbacks, the code starts to be complicated very quickly. Starting with ES6, JavaScript introduced Promises (ES6) and Async/Await (ES2017) as alternatives to Callbacks.

Promises

  • A promise is commonly defined as a proxy for a value that will eventually become available
  • one way to deal with asynchronous code, without getting stuck in callback hell.
  • Promises were standardized and introduced in ES2015, and have recently become more integrated, with async and await in ES2017.

How promise work?

  • Once a promise has been called, it will start in a pending state. This means that the calling function continues executing, while the promise is pending until it resolves, giving the calling function whatever data was being requested.
  • The created promise will eventually end in a resolved state, or in a rejected state, calling the respective callback functions (passed to then and catch) upon finishing.

Example


    const fs = require('fs')

    const getFile = (fileName) => {
      return new Promise((resolve, reject) => {
          fs.readFile(fileName, (err, data) => {
              if (err) {
                  reject(err)   // calling `reject` will cause the promise to fail with or without the error passed as an argument
                  return        // and we don't want to go any further
              }
              resolve(data)
          })
      })
    }

    getFile('/etc/passwd')
        .then(data => console.log(data))
        .catch(err => console.error(err))
        

Chaining promises

  • A promise can be returned to another promise, creating a chain of promises.

    const status = response => {
        if (response.status >= 200 && response.status < 300) {
            return Promise.resolve(response)
        }
        return Promise.reject(new Error(response.statusText))
    }

    const json = response => response.json()

    fetch('/todos.json')
        .then(status)    // note that the `status` function is actually **called** here, and that it **returns a promise***
        .then(json)      // likewise, the only difference here is that the `json` function here returns a promise that resolves with `data`
        .then(data => {  // ... which is why `data` shows up here as the first parameter to the anonymous function
            console.log('Request succeeded with JSON response', data)
        })
        .catch(error => {
            console.log('Request failed', error)
        })
        

Errors

  • When anything in the chain of promises fails and raises an error or rejects the promise, the control goes to the nearest catch() statement down the chain.

    new Promise((resolve, reject) => {
        throw new Error('Error') // or reject('Error')
    }).catch(err => {
        console.error(err)
    });
        

Cascading Errors

  • If inside the catch() you raise an error, you can append a second catch() to handle it, and so on.

    new Promise((resolve, reject) => {
        throw new Error('Error')
    })
    .catch(err => {
        throw new Error('Error')
    })
    .catch(err => {
        console.error(err)
    })
        

Waiting for all promises to resolve: Promise.all()

  • If you need to synchronize different promises, Promise.all() helps you define a list of promises, and execute something when they are all resolved

    Promise.all([p1, p2]).then(res => {
        console.log('Array of results', res)
    })
        

Waiting for any one of the promises to resolve: Promise.race()

  • Promise.race() runs when the first of the promises you pass to it settles (resolves or rejects), and it runs the attached callback just once, with the result of the first promise settled

    Promise.race([p1, p2]).then(result => {
        console.log('result of first promise resolved', result)
    })
        

Waiting for any one of the promise to fulfil: Promise.any()

  • Promise.any() settles when any of the promises you pass to it fulfill or all of the promises get rejected. It returns a single promise that resolves with the value from the first promise that is fulfilled. If all promises are rejected, then the returned promise is rejected with an AggregateError.

    Promise.any([first, second])
        .then(resp => {
            console.log(resp);
        })
        .catch(error => {
            console.log(error) // AggregateError
        })
        

Async/Await

vs Promise

  • is built on promises
  • reduces the boilerplate around promises, and the "don't break the chain" limitation of chaining promises.
  • Promises were introduced to solve the famous callback hell problem, but they introduced complexity on their own, and syntax complexity.
  • Prepending the async keyword to any function means that the function will return a promise. Even if it's not doing so explicitly, it will internally make it return a promise.

    const aFunction = async () => { return 'test' }

    // is similar to

    const aFunction = () => { return Promise.resolve('test') }
      

Working

  • An async function returns a Promise.
  • When you want to call this function you prepend await, and the calling code will stop until the promise is resolved or rejected. One caveat: the client function must be defined as async

    const doSomethingAsync = () => {
        return new Promise(resolve => { setTimeout(() => resolve('I did something'), 3000) })
    }

    const doSomething = async () => { console.log(await doSomethingAsync()) }