Javascript function part 3/4: synchronous and asynchronous

This tutorial is a part of the Learn everything about Javascript in one course.

Simple defintion of synchronous and asynchronous operations

synchronous operation asynchronous operation
Definition Operation where input data is present and ready to be used Operation where input data is not present
Where to get input data No need, it's present Network call, read from drive
Example perform x = 1 + 2 get user data from server,
read video file from drive
(I/O tasks in general)

Defintion of synchronous and asynchronous functions

synchronous function asynchronous function
Definition is function which handles ONLY synchronous operations is function which handles asynchronous operations
Operation can be handled synchronous asynchronous and synchronous
Example function to perform x = 1 + 2 function to:
get user data from server,
read video file from drive
Sequence of code execution line after line as seen in program vary depend on when synchronous operation finishes
absolutely not line after line as seen in program
How to handle returned data (even empty returned data) handle it in following lines of code use callback, Promise or async/await
sync_function.js
// synchronous function
// next line of code executed after previous one finished
console.log('1st line')
let name = 'Tony Stark'
function greet(name) {
  console.log('Hello ' + name + '!')
}
greet(name)
console.log('last line')

// 1st line
// Hello Tony Stark!
// last line
async_function.js
// asynchronous function

console.log('1st line') // 1st line
const username = 'username'
const password = 'password'

function login(username, password) {
  setTimeout(() => {
    console.log('login success with credentials:', username, password)
  }, 1500) // we emulate login result returns after 1.5 second
}

login(username, password)
console.log('last line')

// 1st line
// last line (does not wait for function login() to finish)
// login success with credentials: username password

Callback function, call me when you are done

A callback (callback function):

  • is a function which is called after asynchronous operation is done.
  • is where you can implement post asynchronous operation logic.
  • Call me back when you are done, hence "callback".
  • is passed in as an argument to function which does asynchronous operation.
  • "callback" is extremely popular in Javascript.
callback.js
// use callback functions to handle post asynchronous operation
// (after login)

console.log('1st line')
const username = 'myusername'
const password = 'myPassword'

const successCallback = () => { // a callback function
  console.log('login success, I am doing the next thing')
}

const failureCallback = () => { // a callback function
  console.log('login failed, reporting failure')
}


function login(username, password, successCb, failureCb) {
  setTimeout(() => {
    console.log('credentials:', username, password)
    let ok = true // we assumme login success
    if(ok) {
      successCb() // call success cb when login success
    } else {
      failureCb() // call failure cb if login failed
    }
  }, 1500) // we emulate login result returns after 1.5 second
}

login(username, password, successCallback, failureCallback)
console.log('last line')

// 1st line
// last line
// credentials: myusername myPassword
// login success, I am doing the next thing

You can define a callback function right when passing it in to a function. Two examples below produce the same result.

Separate cb declaration Shorthand cb definition
Declare and call const cb = () => {
console.log('running callback')
}
login(cb)
login(() => {
console.log('running callback'
})

Callback hell: happens when you nest many callback function together. For example: call func1 after login() is done, then call func2 after func1 is done, ... It's hell because it's harder to read.

callback_hell.js
// callback hell
doSomething(function(result) {
  doSomethingElse(result, function(newResult) {
    doThirdThing(newResult, function(finalResult) {
      console.log('Got the final result: ' + finalResult)
    }, failureCallback)
  }, failureCallback)
}, failureCallback)

To make asynchronous logic look prettier and easier to understand, Javascript offers Promise and async/await. We will learn about them next.

Javascript Promise, flatten the callback hell

Declare const promise = new Promise((resolve, reject) => { // do something })

Usage promise.then((resolveVal) => { // do something }).catch((error) => { // handle error })

promise.js
// Promise

console.log('first line') // (I)

const fetchUserInfo = new Promise((resolve, reject) => {
  setTimeout(() => resolve({ name: 'Tony' }) , 1000)
  setTimeout(() => reject('nah') , 2000)
})

fetchUserInfo
.then(resolveVal => console.log(resolveVal)) // (II)
.catch(err => console.log(err))


console.log('beetween promises') // (III)

// Promise is chainable, you can have as many "then" as wanted

fetchUserInfo
.then(resolveVal => 'Hello ' + resolveVal.name + '!') // value returned by this "then"
.then(greet => console.log(greet)) // (IV), is input of next "then"
.then() // you can chain as many "then"
.then() // as needed
.then() // even with empty ones
.then(() => fetchUserInfo) // you can aslo return a promise
.then(val => console.log(val)) // (V), this is how we "flatten" the callback hell
.catch(err => console.log(err))

console.log('last line') // (VI)

// first line (I)
// beetween promises (III)
// last line (VI)
// { name: 'Tony' } (II)
// Hello Tony! (IV)
// { name: 'Tony' } (V)

Promise.all(), wait for all given promises to finish

The Promise.all() method returns a single Promise that fulfills when all given promises have been fulfilled. All promises run in parallel.

Syntax Promise.all([promise1, promise2, ...]).then(([value1, value2, ...]) => { // do something })

promise.js
// Promise.all() run all promise in parallel and
// get the result when all of them finish

console.time('top_to_bottom')
console.time('start_to_finish_all_promise')
console.time('start_to_finish_promise_2')
console.time('start_to_finish_promise_3')

const promise1 = Promise.resolve(3)
const promise2 = new Promise((resolve) => {
  setTimeout(() => {
    resolve('foo')
    console.timeEnd('start_to_finish_promise_2')
  }, 900)
})
const promise3 = new Promise((resolve) => {
  setTimeout(() => {
    resolve('bar')
    console.timeEnd('start_to_finish_promise_3')
  }, 300)
})

Promise.all([promise1, promise2, promise3]).then(function(values) {
  console.log(values);
  console.timeEnd('start_to_finish_all_promise')
});
console.timeEnd('top_to_bottom')

// top_to_bottom: 0.594ms
// start_to_finish_promise_3: 305.773ms
// start_to_finish_promise_2: 906.264ms
// [ 3, 'foo', 'bar' ]
// start_to_finish_all_promise: 908.998ms

async/await syntax to improve code readability of async logic

async function, syntax async function name() {...} or async () => {...}:

  • defines an asynchronous function.
  • returns an implicit Promise.
  • syntax and structure of code using async functions looks like synchronous functions.

await operator, syntax value = await aPromise or value = await asyncFuntion():

  • is used to wait for a Promise.
  • can only be used inside an async function.

Comparision of callback, Promise and async/await:

callback Promise async / await
Purpose: to handle returned data from asynchronous operation
Can be used to create asynchronous operation
Where logic to handle returned data from asynchronous operation lives within callback function within then(...) part within async function
How to wait for multiple asynchronous operations sequencially nested callback (hell) chained "then" .then(..).then(..) use multiple await
How to wait for multiple asynchronous operations in parallel ❌ (extra complicated logic) use Promise.all() use Promise.all()
How nested async code look like (callback) hell "flatten", but a lot of .then(...) like synchronous code
Code readability worst somewhat ok easiest to read
Suggested usage minimal minimal as much as possible
async.await.js
// async/await syntax to improve code readability of async logic

// 3 different styles of handling returned data from 
// fetchUser asynchronous operation

console.time('top_to_bottom')
console.time('top_to_callback')
console.time('top_to_promise')
console.time('top_to_async_await')

// callback style
const handleUserDataCallback = (val) => {
  console.log('handle user data, callback style:', val)
  console.timeEnd('top_to_callback')
}

function fetchUserCallbackStyle(callback) {
  setTimeout(() => {
    callback({ user: 'Superman', superpower: 'fly' })
  }, 1000)
}

fetchUserCallbackStyle(handleUserDataCallback)
async.await.js
// promise style
const fetchUserPromiseStyle = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve({ user: 'Aquaman', superpower: 'dive' })
  }, 2000)
})

fetchUserPromiseStyle
.then(val => {
  console.log('handle user data, promise style:', val)
  console.timeEnd('top_to_promise')
})
.catch(err => console.log(err))
async.await.js
// async/await style
const fetchUserAsyncFunctionStyle = async () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve({ user: 'Deadpool', superpower: 'indestructible' })
    }, 3000)
  })
}

const handleUserDataAsyncFunctionStyle = async () => {
  const val = await fetchUserAsyncFunctionStyle() // also see the difference of
  const val2 = await fetchUserPromiseStyle // calling promise and function here
  console.log('handle user data, async function style:', val)
  console.log('handle user data, async function style:', val2)
  console.timeEnd('top_to_async_await')
}

handleUserDataAsyncFunctionStyle()

console.timeEnd('top_to_bottom')

// top_to_bottom: 0.759ms
// handle user data, callback style: { user: 'Superman', superpower: 'fly' }
// top_to_callback: 1006.114ms
// handle user data, promise style: { user: 'Aquaman', superpower: 'dive' }
// top_to_promise: 2006.171ms
// handle user data, async function style: { user: 'Deadpool', superpower: 'indestructible' }
// handle user data, async function style: { user: 'Aquaman', superpower: 'dive' }
// top_to_async_await: 3002.161ms

Summary

  • callback, Promise and await/async are used for handling post-asynchronous operation logic.
  • Although await/async is in favor to use, it's built upon callback and Promise. So it's extremely important to know them all, especially the comparision table.