< / >
Published on

Asynchronous JavaScript

Table of Contents

Callbacks

Callbacks are functions that are passed as arguments to other functions and are executed at a later time, typically after an asynchronous operation completes. They are a fundamental concept in JavaScript for handling asynchronous tasks.

Example:

function fetchData(callback) {
  setTimeout(() => {
    callback('Data fetched successfully')
  }, 2000)
}

function processData(data) {
  console.log('Processing data:', data)
}

fetchData(processData) // Output: Processing data: Data fetched successfully

In this example, fetchData simulates fetching data asynchronously and calls the provided callback function (processData) when the data is available.

Promises

Promises provide a cleaner and more flexible approach to handle asynchronous operations compared to callbacks. A Promise represents a value that may be available now, in the future, or never. It has three states: pending, fulfilled, or rejected.

Example:

function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('Data fetched successfully')
    }, 2000)
  })
}

fetchData()
  .then((data) => {
    console.log('Processing data:', data)
  })
  .catch((error) => {
    console.error('Error fetching data:', error)
  })

Here, fetchData returns a Promise that resolves with the fetched data. We can use the then method to handle the resolved value and the catch method to handle errors.

Promises in JavaScript can be classified into different types based on their behavior and purpose. Here are some common types of Promises:

Standard Promises

These are the basic Promises that are used to represent the eventual completion (or failure) of an asynchronous operation. They are created using the Promise constructor.

Example:

let promise = new Promise((resolve, reject) => {
  // Async operation
  if (/* operation successful */) {
    resolve("Operation completed successfully");
  } else {
    reject(new Error("Operation failed"));
  }
});

promise.then(result => {
  console.log(result);
}).catch(error => {
  console.error(error);
});

Chained Promises

Chaining Promises allows you to sequence multiple asynchronous operations. Each then call returns a Promise, allowing you to chain multiple asynchronous operations together.

Example:

function fetchUserData() {
  return new Promise((resolve, reject) => {
    // Fetch user data
    resolve({ name: 'Kumar', age: 30 })
  })
}

function fetchUserPosts(user) {
  return new Promise((resolve, reject) => {
    // Fetch user posts based on user data
    resolve(['Post 1', 'Post 2'])
  })
}

fetchUserData()
  .then((user) => fetchUserPosts(user))
  .then((posts) => console.log(posts))
  .catch((error) => console.error(error))

Parallel Promises

Sometimes, you need to perform multiple asynchronous operations concurrently and wait for all of them to complete. You can achieve this using Promise.all, which takes an array of Promises and returns a single Promise that resolves when all of the input Promises have resolved.

Example:

let promise1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('Promise 1 resolved')
  }, 2000)
})

let promise2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('Promise 2 resolved')
  }, 1000)
})

Promise.all([promise1, promise2])
  .then((results) => {
    console.log(results)
  })
  .catch((error) => {
    console.error(error)
  })

Race Promises

Sometimes, you need to perform multiple asynchronous operations concurrently and only need to wait for the first one to complete. You can achieve this using Promise.race, which takes an array of Promises and returns a single Promise that resolves or rejects as soon as one of the input Promises resolves or rejects.

Example:

let promise1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('Promise 1 resolved')
  }, 2000)
})

let promise2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('Promise 2 resolved')
  }, 1000)
})

Promise.race([promise1, promise2])
  .then((result) => {
    console.log(result)
  })
  .catch((error) => {
    console.error(error)
  })

Async/await

Async/await is a syntactic sugar built on top of Promises, making asynchronous code look and behave more like synchronous code. The async keyword is used to declare an asynchronous function, and the await keyword is used to wait for a Promise to resolve or reject within an async function.

Example:

async function fetchData() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve('Data fetched successfully')
    }, 2000)
  })
}

async function processData() {
  try {
    const data = await fetchData()
    console.log('Processing data:', data)
  } catch (error) {
    console.error('Error fetching data:', error)
  }
}

processData()

In this example, fetchData returns a Promise, and processData waits for the Promise to resolve using await. The try-catch block is used to handle errors.

Event Loop

The Event Loop is the heart of JavaScript's asynchronous non-blocking behavior. It continuously checks the call stack and the callback queue. When the call stack is empty, it picks up tasks from the callback queue and pushes them onto the call stack for execution.

Example:

console.log('Start')

setTimeout(() => {
  console.log('Timeout')
}, 0)

console.log('End')

Output:

Start
End
Timeout

In this example, even though setTimeout is called with a delay of 0 milliseconds, it is still executed after End because JavaScript schedules it in the callback queue, and the Event Loop picks it up after the main thread completes execution.

Call Stack

Call Stack is a data structure that stores function calls in a last-in, first-out (LIFO) manner. When a function is called, it is pushed onto the stack, and when it returns, it is popped off the stack. This mechanism allows JavaScript to keep track of function calls and their execution context.

Example:

function firstFunction() {
  console.log('Inside firstFunction')
  secondFunction()
}

function secondFunction() {
  console.log('Inside secondFunction')
}

firstFunction()

In this example, when firstFunction() is called, it gets added to the call stack. Inside firstFunction(), console.log() is called to print a message, and then secondFunction() is called. So, secondFunction() is added on top of firstFunction() in the call stack. Inside secondFunction(), another console.log() is called.

Output:

Inside firstFunction
Inside secondFunction

The call stack looks like this:

  • secondFunction() (top of the stack)
  • firstFunction()

When secondFunction() completes its execution, it is removed from the stack, and then firstFunction() completes and is removed from the stack as well.

Example: Let's add another function call inside secondFunction():

function firstFunction() {
  console.log('Inside firstFunction')
  secondFunction()
}

function secondFunction() {
  console.log('Inside secondFunction')
  thirdFunction()
}

function thirdFunction() {
  console.log('Inside thirdFunction')
}

firstFunction()

In this case, the call stack will look like this:

  • thirdFunction() (top of the stack)
  • secondFunction()
  • firstFunction()

As each function completes its execution, it will be removed from the stack in reverse order until the stack becomes empty.

Output:

Inside firstFunction
Inside secondFunction
Inside thirdFunction

Callback Queue

Callback Queue, also known as the Task Queue or Message Queue, stores callbacks of completed asynchronous operations. When an asynchronous operation completes, its callback is pushed onto the Callback Queue.

  • The callback queue holds functions that are queued to be executed once the call stack is empty.
  • Asynchronous operations, like timers, network requests, or user input events, use callback functions to handle their completion.
  • The event loop continuously checks the call stack and the callback queue, ensuring that functions are executed in the correct order and that the JavaScript runtime remains responsive.