Asynchronous JS and the Event Loop

Asynchronous JavaScript

  • When you see the term Asynchronous you should question yourself, do we know Synchronous?
     
  • The Javascript code gets executed in 2 different ways:
    Synchronous, and
    Asynchronous are those exact 2 ways.
console.log("start")

for (let i=0; i < 6; i+=1) {
  console.log({i})
}

console.log("end")

// Output
/**
 * start
 * 0
 * 1
 * 2
 * 3
 * 4
 * 5
 * end
 * */
  • Simple JavaScript code gets executed lexically, or Synchronously, i.e, the code is executed in the order they are written.

Asynchronous JavaScript

console.log("start")

for (let i=0; i < 6; i+=1) {
  console.log({i})
}

console.log("end")

// Output
/**
 * start
 * 0
 * 1
 * 2
 * 3
 * 4
 * 5
 * end
 * */
  • But if the for_loop written here takes a lot of time to complete?
  • It will block the console.log("end") from executing.
  • But if there could be a way to by-pass time-taking for_loop or to put the for_loop on a side-thread and execute the next code statement?
  • Then we would not be blocked in the code.

Asynchronous JavaScript

const users = [
  "asdas1wwq23wd3", 
  "wwq23wd3wwq23wd3", 
  "q23wd3wwq23wd3ww" ] // ...100_000 more
const authenticatedUser = "qaaqq13oopf233ww";

let currentUser; // undefined

for (let i=0; i < 6; i+=1) {
  if (users[i] === authenticatedUser) {
    currentUser = users[i]
  }
}

console.log(currentUser)

// Output
// undefined
  • Further situation: we put the time taking for_loop on a side thread and moved on to execute the console.log("end"). It worked fine.
     
  • What if the next line of code (console) was dependent on the for_loop? How will we go ahead by side threading the for_loop?
     
  • This is where asynchronous programming kicks in.

Asynchronous JavaScript

const users = [
  "asdas1wwq23wd3", 
  "wwq23wd3wwq23wd3", 
  "q23wd3wwq23wd3ww" ] // ...100_000 more
const authenticatedUser = "qaaqq13oopf233ww";

let currentUser; // undefined

for (let i=0; i < 6; i+=1) {
  if (users[i] === authenticatedUser) {
    currentUser = users[i]
  }
}

console.log(currentUser)

// Output
// undefined
  • Asynchronous programming in JavaScript is:

    When there's a line of code, that will take a long time to execute,  you can ask the JS engine to put that code aside, and start with the next line of code.
    Once the JS engine has executed all the lines of code, and is free, it comes back to the asynchronous code, and executes it.
     
  • How can you ask the JS engine to put the code aside?

Asynchronous JavaScript

const users = [
  "asdas1wwq23wd3", 
  "wwq23wd3wwq23wd3", 
  "q23wd3wwq23wd3ww" ] // ...100_000 more
const authenticatedUser = "qaaqq13oopf233ww";

function getCurrentUser(data, callback) {
  const { users, authenticatedUser } = data
  let currentUser; // undefined
  for (let i=0; i < 6; i+=1) {
	if (users[i] === authenticatedUser) {
	  currentUser = users[i]
	}
  }
  if (currentUser) {
    callback(currentUser)
  }
}

getCurrentUser({users, authenticatedUser}, (err, data) => {
  console.log(data)
})

You can try 3 things here:

  1. callbacks - write a function just after for-loop, as a callback. This function will execute only once the for-loop has executed. You can put the console statement there.
     
  2. Promises - create a Promise object. Add your for_loop inside the promise. And when you consume the promise, put your console statement there.
     
  3. Async/await

Asynchronous JavaScript

Promises:

The concept of using promises has 2 steps to it:

  1. creating a promise
  2. consuming a promise


Creating Promise:
The promise constructor, receives 1 argument, an executor function.

This executor function also takes 2 arguments, both of them functions:
resolve, and reject

const myPromise = new Promise(() => {});

const myPromise = new Promise((resolve, reject) => {});

const promise = new Promise(
  (resolve, reject) => {
  const num = Math.random()*100
  if (num > 200){
    resolve("done")
  } else {
    reject("")
  }
});

Asynchronous JavaScript

A promise is always in one the following states:

  1. pending: initial state, neither fulfilled nor rejected.
  2. fulfilled: meaning that the operation was completed successfully.
  3. rejected: meaning that the operation failed.

Asynchronous JavaScript

Consuming a Promise:

Matching resolve, and reject with .then and .catch
 

  • resolve and reject receive only 1 argument, ignore more
  • resolve returns to .then
  • reject returns to .catch
  • finally runs anyway.
const parameter = "success"

const myPromise = new Promise((resolve, reject) => {
    // Simulating an asynchronous operation
    setTimeout(() => {
      if (parameter === 'success') {
        resolve('Operation succeeded');
      } else {
        reject(new Error('Operation failed'));
      }
    }, 1000); // Simulating 1 second delay
  });

// consuming a promise
myPromise
  .then(result => {
    console.log('Success:', result);
  })
  .catch(error => {
    console.error('Error:', error.message);
  })
  .finally(() => {
    console.log(`This will be executed regardless of 
                success or failure.`);
  });

Promise has 2 states (resolve, reject) because:

in case the code you have written throws some error, the promises goes in the rejected state.

Asynchronous JavaScript

Using .finally:
 

  1. receives a function as arg,
  2. this function receives no arg,
  3. return value of this function is ignored,
  4. runs after promise is fulfilled, but
  5. finally doesn't know if promise resolved or rejected
  6. only if, finally throws error, it passes on to error handler
const parameter = "success"

const myPromise = new Promise((resolve, reject) => {
    // Simulating an asynchronous operation
    setTimeout(() => {
      if (parameter === 'success') {
        resolve('Operation succeeded');
      } else {
        reject(new Error('Operation failed'));
      }
    }, 1000); // Simulating 1 second delay
  });

// consuming a promise
myPromise
  .then(result => {
    console.log('Success:', result);
  })
  .catch(error => {
    console.error('Error:', error.message);
  })
  .finally(() => {
    console.log('This will be executed regardless of success or failure.');
  });

Asynchronous JavaScript

Practically using Promises:

  • Promisification: functions returning a promise.
    Eg: the "fetch" function (a WebAPI)
function myAsyncFunction(parameter) {
  return new Promise((resolve, reject) => {
    // Simulating an asynchronous operation
    setTimeout(() => {
      if (parameter === 'success') {
        resolve('Operation succeeded');
      } else {
        reject(new Error('Operation failed'));
      }
    }, 1000); // Simulating 1 second delay
  });
}

myAsyncFunction('success')
  .then(result => {
    console.log('Success:', result);
  })
  .catch(error => {
    console.error('Error:', error.message);
  })
  .finally(() => {
    console.log(`This will be executed 
	regardless of success or failure.`);
  });

Asynchronous JavaScript

But why do we need to wrap Promises inside a function:

  1. Function have lazy execution
  2. Promises have eager execution
    i.e, any JS code directly inside the executor function, will get executed synchronously as soon as the JS Interpreter reaches the Promise declaration.
  3. This is because the Promise constructor has to call and execute the argument (executor function) before it can return the Promise object.
  4. Read the "NOTE" on the JS Spec about Promises
new Promise((res, rej) => {})

/*
The Promise constructor gets called,
It constructs a Promise object,
And returns it to the 'myPromise' variable
In order to contruct the Promise object,
the constructor has to execute any JS code
passed to it (through the executor function)

*/

const myPromise = new Promise((resolve, reject) => {
    const results = [];
    for (let i = 0; i < 10_000; i++) {
      console.log({i})
      results.push(i)
    }
    resolve(results)
});

myPromise
.then(x => console.log(x))

Asynchronous JavaScript

  1. The executor function is called for initiating and reporting completion of the possibly deferred action represented by this Promise.
  2. The executor is called with two arguments: resolve and reject.
  3. These are functions that may be used by the executor function to report eventual completion or failure of the deferred computation.
  4. Returning from the executor function does not mean that the deferred action has been completed but only that the request to eventually perform the deferred action has been accepted.

Asynchronous JavaScript

Practically using Promises:

  • chaining multiple .then

    - .then
    receives one function as an argument,
    - if this function returns something you get that in the next .then in the chain
    -  the function in .then can return a promise or non-promise value
fetch("https://jsonplaceholder.typicode.com/posts")
.then(response => {
  return response.json()
})
.then(jsonData => {
  console.log({jsonData})
  return jsonData
})
.then(data => {
  console.log({data})
  return fetch("https://jsonplaceholder.typicode.com/todos/1")
})
.then(todoData => {
  return todoData.json()
})
.catch(error => {
  console.error('Error:', error.message);
})
.finally(() => {
  console.log(`This will be executed 
  regardless of success or failure.`);
});
.then(todoJsonData => {
  console.log({todoJsonData})
})

Asynchronous JavaScript

Practically using Promises:

 

  • if nothing is returned from the .then function, the next .then method will still be executed,
    it will just get the value as undefined
doSomething()
  .then((url) => {
    // Missing `return` keyword in front of fetch(url).
    fetch(url);
  })
  .then((result) => {
    // result is undefined, 
    // because nothing is returned from the previous
    // handler. There's no way to know 
    // the return value of the fetch()
    // call anymore, or whether it succeeded at all.
  });

doSomething()
  .then((result) => {
    return doSomethingElse(result);
  })
  .then((newResult) => {
    return doThirdThing(newResult);
  })
  .then((finalResult) => {
    console.log(`Got the 
		final result: ${finalResult}`);
  })
  .catch(failureCallback);

Asynchronous JavaScript

function asyncOperation(success) {
  return new Promise((resolve, reject) => {
    if (success) {
      // Resolving the promise
      resolve("completed"); 
    } else {
      // Rejecting the promise
      reject(new Error("failed"));
    }
    
    // Attempting to resolve/reject 
    // the promise again will have no effect
    resolve("Won't Run");
    reject(new Error("Won't run"));
  });
}

// Resolving example
asyncOperation(true)
  .then((result) => {
    console.log("Resolved:", result);
  })
  .catch((error) => {
    console.error("Error:", error.message);
  });

Practically using Promises:

 

A promise can be "fulfilled" only once, with either resolve or reject

Eg: You cannot write a loop with resolve or reject, inside a Promise.

Asynchronous JavaScript

function asyncOperation() {
  return new Promise((resolve, reject) => {
    const results = [];

    // Simulating a for loop inside 
    // the promise executor function
    for (let i = 0; i < 5; i++) {
      resolve(i);
    }
  });
}

Practically using Promises:


A promise can be "fulfilled" only once, with either resolve or reject

Eg:
You cannot write a loop with resolve or reject, inside a Promise.

Asynchronous JavaScript

const prom = new Promise((resolve, reject) => {
  const results = [];
  for (let i = 0; i < 10_000; i++) {
    console.log({i})
    results.push(i)
  }
  resolve(results)
});

prom
  .then(data => console.log({data}))

prom
  .then(data => console.log({data}))

prom
  .then(data => console.log({data}))

function asyncOperation() {
  return new Promise((resolve, reject) => {
    const results = [];
    for (let i = 0; i < 10_000; i++) {
      console.log({i})
      results.push(i)
    }
    resolve(results)
  });
}

Practically using Promises:

 

If a promise is once resolved (made an API call), it won't have to resolve again. If you again consume the promise, it will give the exact same result (won't call the API again).

Note: This doesn't apply if you have wrapped a Promise inside a function.

Asynchronous JavaScript

  • Replacing Promise wrapped inside a function with Async/Await syntax
  • because of the eager execution of the promises, they are used wrapped inside a function. To simplify this, JS brought "async" keyword. Now if you add "async" before any function, it automatically calls the function constructor and returns a Promise object. So you don't have to write "return new Promise".
const promisified = (api) => {
  return new Promise((res, rej) => {
    const getResult = callApiToGetResult(api)
    
    if ( getResult ) {
      res(getResultJson)  
    }
    rej("Rejected")
  })
}

promisified("https://result.api/")
  .then(() => {}, () => {})


// Can be converted to an async function:
const promisified = async (api) => {
    const getResult = callApiToGetResult(api)
    return getResult
}

// calling promisified() because its a function
promisified("https://result.api/")
  .then(() => {}, () => {})

Asynchronous JavaScript

  • Await: now inside the promise if you have added some code that takes a lot of time, let's say reading a file or loading a file etc, and you want the promise to resolve only after the file data has loaded.
  • But because of the eager execution, the "resolve" doesn't wait for the data to come.
  • So you have to use some trick to make it wait and execute only after a condition is fulfilled (eg: event listeners, or setTimeout etc)
  • To solve this: we got "await". No tricks needed, directly use await to block the code.
const promisified = (api) => {
  return new Promise((res, rej) => {
    const getResult = callApiToGetResult(api)
    
    if ( getResult ) {
      res(getResultJson)  
    }
    rej("Rejected")
  })
}

promisified("https://result.api/")
  .then(() => {}, () => {})


// Can be converted to an async function:
const promisified = async (api) => {
    const getResult = callApiToGetResult(api)
    return getResult
}

// calling promisified() because its a function
promisified("https://result.api/")
  .then(() => {}, () => {})

Asynchronous JavaScript

When consuming a promise, the .then doesn't run un till the resolve inside the promise is called.

 

await stops the code at that line, and moves to the next line only after that line has executed completely i.e only when the promise has resolved.

 

await can only be used inside an async function, because only inside a promise you'd want to write any blocking code.


// Can be converted to an async function:
const promisified = async (api) => {
    const getResult = await fetch(api)
    const getResultJson = await getResult.json()
    return getResultJson
}

// calling promisified() because its a function
promisified("https://result.api/")
  .then((resp) => {
    console.log({resp})
  })
  .catch((error) => {
    console.error({error})
  })

Asynchronous JavaScript

async function getDataPromise(API){
    const data = await fetch(API)
    const jsonData = await data.json()
    return jsonData;
  })
}

const API = 'https://jsonplaceholder.typicode.com/posts'

async function finalDataFunction(API){
  const res = await getDataPromise(API)
  console.log({res})
}

finalDataFunction(API)
function getDataPromise(API){
  return new Promise((res, rej) => {
    fetch(API)
      .then(data => data.json())
      .then(jsonData => res(jsonData))
  })
}

const API = 'https://jsonplaceholder.typicode.com/posts'

getDataPromise(API)
.then(res => console.log({res}})

Comparing Fetch and Async / Await

Asynchronous JavaScript

Error Handling in Promises:

  • If there's an error or exception the browser will look down the chain for .catch() handler,
  • in a chain, only one .catch can be enough,
  • multiple .catch can be added for handling below cases
  • .then can be added after .catch
  • .then methods added after .catch run anyway
doSomething() // if error
  .then((result) => doSomethingElse(result)) // won't run
  .then((newResult) => doThirdThing(newResult)) // won't run
  .then((finalResult) => console.log({finalResult}) // won't run
  .catch(err => console.log({err})) // Runs to throw error
  .then((newResult) => console.log('1')) // runs anyway
  .then((newResult) => console.log('2')) // runs anyway
        
        
        
// Case for having multiple .catch in the chain
doSomething() // if error
  .then((result) => doSomethingElse(result)) // won't run
  .then((newResult) => doThirdThing(newResult)) // won't run
  .then((finalResult) => console.log({finalResult}) // won't run
  .catch(err => console.log({err})) // Runs to throw error
  .then((newResult) => console.log('1')) // runs anyway
  .then((newResult) => console.log('2')) // runs anyway
  .catch(err => console.log({err})) 
        // Runs if error in 1 of above 2 .thens

Asynchronous JavaScript

Error Handling in Promises:

  • handling resolve and reject both with .then
  • .catch is just a syntactic sugar for .then receiving 2 arguments
doSomething()
  .then(resp => console.log({resp})
  .catch(err => console.log({err}))

// can be written as
doSomething()
  .then(() => {}, () => {})

Asynchronous JavaScript

Event Loop:

Callback Queue:  For setTimeout, setIntervals etc

Asynchronous JavaScript

Event Loop:

JavaScript has a runtime model based on an event loop, which is responsible for executing the code, collecting and processing events, and executing queued sub-tasks.

This model is quite different from models in other languages like C and Java.

 

Ref: https://dev.to/lydiahallie/javascript-visualized-event-loop-3dif

Asynchronous JavaScript

  • Promise.resolve – makes a resolved promise with the given value.
  • Promise.reject – makes a rejected promise with the given error.
  • Promise.all – waits for all promises to resolve and returns an array of their results. If any of the given promises rejects, it becomes the error of Promise.all, and all other results are ignored.
  • Promise.allSettled (recently added method) – waits for all promises to settle and returns their results as an array of objects with:
    status: "fulfilled" or "rejected"
    value (if fulfilled) or reason (if rejected).
  • Promise.race – waits for the first promise to settle, and its result/error becomes the outcome.
  • Promise.any (recently added method) – waits for the first promise to fulfill, and its result becomes the outcome. If all of the given promises are rejected, AggregateError becomes the error of Promise.any.
  • Of all these, Promise.all is probably the most common in practice.
Promise.resolve()
Promise.reject()
Promise.all()
Promise.allSettled()
Promise.any()
Promise.race()

There are 6 static methods of Promise class:

Asynchronous JavaScript

Direct resolve and reject:

const resolvedPromise = Promise.resolve("Resolved value");
resolvedPromise.then(value => {
  console.log(value); // Output: Resolved value
});

const rejectedPromise = Promise.reject(new Error("Rejected promise"));
rejectedPromise.catch(error => {
  console.error(error.message); // Output: Rejected promise
});

Asynchronous JavaScript

Promise.all: (all or nothing)

  • Takes an array of promises,
  • returns a promise which resolves to an array which has resolved value of all the promises,
  • resolved array of promises is in the same order of input
  • resolves all of them in parallel,
  • in case a promise gets rejected, the promises after that do not get executed at all,
  • in case a promise gets rejected, the promises before that will be fulfilled,
  • non-promise values are also allowed in the array
const urls = [
  'https://api.github.com/users/iliakan',
  'https://api.github.com/users/remy',
  'https://api.github.com/users/jeresig'
];

// map every url to the promise of the fetch
const requests = urls.map(url => fetch(url));

// Promise.all waits until all jobs are resolved
Promise.all(requests)
  .then(responses => responses.forEach(
    response => alert(`${response.url}: ${response.status}`)
  ));


const promise1 = Promise.resolve("Promise 1 resolved");
const promise2 = Promise.reject("Rejected promise");
const promise3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, "Promise 3 resolved");
});

Promise.all([promise1, promise2, promise3]).then(values => {
  console.log(values); // Output: ["Promise 1 resolved", 42, "Promise 3 resolved"]
});

Asynchronous JavaScript

Promise.allSettled

  • similar to Promise.all, but it doesn't stop at first rejected promise,

  • just waits for all promises to settle, regardless of the result.

  • The resulting array has:

    {status:"fulfilled", value:result} for successful responses,
    {status:"rejected", reason:error} for errors.

  • Order of output array remains same as input array,

  • resolves all of them in parallel,
const promise1 = Promise.resolve("Resolved");
const promise2 = Promise.reject(new Error("Rejected"));

Promise.allSettled([promise1, promise2]).then(results => {
  console.log(results);
  /* Output:
    [
      { status: "fulfilled", value: "Resolved" },
      { status: "rejected", reason: Error: Rejected }
    ]
  */
});

// [
//   {status: 'fulfilled', value: ...response...},
//   {status: 'fulfilled', value: ...response...},
//   {status: 'rejected', reason: ...error object...}
// ]

Asynchronous JavaScript

Promise.race

  • similar to Promise.all,
  • executes all the promises in parallel,
  • waits only for the first settled (resolved or rejected) promise and returns with its result (or error)
  • After the first settled promise “wins the race”, all further results/errors are ignored.
Promise.race([
  new Promise((res, rej) => setTimeout(() => res(1), 1000)),
  new Promise((res, rej) => setTimeout(() => rej(new Error("Whoops!")), 2000)),
  new Promise((res, rej) => setTimeout(() => res(3), 3000))
])
  .then(alert); // 1

Asynchronous JavaScript

Promise.any

  • Similar to Promise.race, but waits only for the first resolved promise and gets its result.
Promise.any([
  new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 1000)),
  new Promise((resolve, reject) => setTimeout(() => resolve(1), 2000)),
  new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
])
  .then(alert); // 1

Asynchronous JavaScript

  • AbortController: Aborting Promises,
  • Debounce,
  • Throttle,

Performance Optimizations In Async JavaScript:

Asynchronous JavaScript

AbortController: Aborting Promises:

AbortController can be used to abort/stop any ongoing asynchronous task (Promise), including fetch as well.

Using an AbortController object is very straightforward:

// Step 1. Create a new AbortController Object
let controller = new AbortController();
// controller = {
//   signal: {},
//   abort: () => {}
// }

// call the abort method from where you want to abort
// the ongoing request
controller.abort(); // abort!


// use the signal property to cancel any ongoing request
let signal = controller.signal;
signal.addEventListener('abort', () => {
  alert("abort!") //. whatever you want to do when the
  // request is cancelled!
});

// The event triggers and signal.aborted becomes true
alert(signal.aborted); // true

Asynchronous JavaScript

AbortController: Aborting Promises:

Using AbortController to cancel an ongoing fetch request:
To be able to cancel a fetch, pass the signal property of an AbortController as a fetch option:

let controller = new AbortController();
fetch(url, {
  // pass signal to fetch, in the options object
  signal: controller.signal
});


// call the abort method when 
// you want to cancel request
controller.abort();

Asynchronous JavaScript

// abort in 1 second
let controller = new AbortController();

// calling abort after 1s
setTimeout(() => controller.abort(), 1000);

try {
  let response = await fetch('https://jsonplaceholder.typicode.com/posts', {
    // passing signal to fetch
    signal: controller.signal
  });
} catch(err) {
  if (err.name == 'AbortError') { // handle abort()
    alert("Aborted!");
  } else {
    throw err;
  }
}

Note: When a fetch is aborted, its promise rejects with an error AbortError, so we can handle it, e.g. in a try..catch or a .then/.catch

Asynchronous JavaScript

const urls = [...]; // a list of urls to fetch in parallel

const controller = new AbortController();

// an array of fetch promises
const fetchJobs = urls.map(url => fetch(url, {
  signal: controller.signal
}));

const results = await Promise.all(fetchJobs);

// if controller.abort() is called from anywhere,
// it aborts all fetches

Note: AbortController allows to cancel multiple promises/fetch requests at once as well.

Create a new AbortController, and pass the same signal object to all the promises/fetch requests.

When you call the abort() method, it cancels all the promises having that signal.

Asynchronous JavaScript

const controller = new AbortController();

// our task
const ourJob = new Promise((resolve, reject) => {
  ...
//   Some async code executing
  ...
  controller
    .signal
    .addEventListener('abort', () => reject("Abort called"));
});

// calling abort after 1s
setTimeout(() => controller.abort(), 1000);
// Wait for fetches and our task in parallel

Note: Aborting a simple promise using AbortController

Asynchronous JavaScript

const debounceFunc = (func, delay) => {
   let timer;
    return function(...args) {
       const context = this;
       clearTimeOut(timer);
      
       timer = setTimeOut(() => {
           func.apply(context, args);
       }, delay)
     }
}
const optimisedSearchHandler = debounceFunc(searchHandler, 500)

Debounce

Debouncing enforces that a function not be called again until a certain amount of time has passed without it being called.

As in “execute this function only if 100 milliseconds have passed without it being called.”

 

Only the last call made in a time frame will go through.

Asynchronous JavaScript

const throttleFunc = (func, interval) => {
   let shouldFire = true;
   return function() {
     if (shouldFire) {
         func();
         shouldFire = false;
       
         setTimeOut(() => {
           shouldFire = true;
          }, interval)
      }
   }
}
const optimisedTriggerHandler = throttleFunc(handlerTrigger, 100);

Throttling

Throttling enforces a maximum number of times a function can be called over time. As in “execute this function at most once every 100 milliseconds.”

 

All the calls will go through, but only after fixed time interval

Asynchronous JavaScript