- 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 userinput events
, use callback functions to handle their completion. - The
event loop
continuously checks thecall stack
and thecallback queue
, ensuring that functions are executed in the correct order and that the JavaScript runtime remains responsive.