Understanding the power of asynchronous code and how to simplify its use cases
Contents
- Asynchronous vs Synchronous Code
- Callbacks
- Promises
- Async/Await
Asynchronous vs Synchronous Code
Synchronous code runs in a sequence which means that each operation must wait for the previous one to complete before executing. This code is considered "blocking" since it blocks code from being run.
console.log('One')
console.log('Two')
console.log('Three')
// LOGS: 'One', 'Two', 'Three'
Asynchronous code is non-blocking and runs in parallel with other functions. This means that an operation can occur while another one is still being processed.
console.log('One')
setTimeout(() => console.log('Two'), 100)
console.log('Three')
// LOGS: 'One', 'Three', 'Two'
"Keep in mind that JavaScript is a single threaded language, which means it runs at most one line of JS code at a time."
There are three patterns we can use when working with asynchronous code:
Callbacks
Promises
Async/Await
Callbacks
When using asynchronous functions, we can have a callback as one of the parameters.
// Asynchronous
console.log('Before')
getUser(1, (user) => {
getRepositories(user.gitHubUsername, (repos) => {
console.log('Repos', repos)
})
})
console.log('After')
function getUser(id, callback) {
setTimeout(() => {
console.log('Hello')
callback({ id: id, gitHubUsername: 'Tandid'})
}, 2000)
}
function getRepositories(username, callback){
setTimeout(() => {
console.log('Hello again')
callback(['repo1', 'repo2'])
}, 2000)
}
The problem with using callbacks however is that there becomes a deeply nested structure that can get extremely messy. This is known as Callback Hell. The more functions you invoke, the messier the code gets.
In comparison, the synchronous implementation for this code is a lot easier to read.
//Synchronous
console.log('Before')
const user = getUser(1)
const repos = getRepositories(user.gitHubUsername)
console.log('After')
To resolve the Callback Hell problem, that's where promises come in. You can also resolve this with named functions but this isn't efficient enough.
Promises
A promise is an object that holds the eventual result of an asynchronous operation. When an asynchronous operation completes, it can result in a value or an error.
A promise has three states
Pending
Fulfilled
Rejected
Pending state occurs when you first create the promise and it is starting up the async operation
When results are ready, the promise will be in the Fulfilled state, which means the operation went successfully.
When something goes wrong during the process, the promise will be in the Rejected state, meaning there was an error.
Let's take a look at the following code below to understand Promises more.
const p = new Promise((resolve, reject) => {
// Kick off some async work
// ...
setTimeout(() => {
resolve(1); // pending => resolved, fulfilled
reject(new Error('message')); // pending => rejected
}, 2000);
});
p
.then(result => console.log('Result', result))
.catch(err => console.log('Error', err.message));
When first creating a Promise, we will pass in a function with the parameters resolve and reject. Resolve is used when successfully getting a result from a promise and reject is what happens when the promise fails.
Next, we need to consume the promise by using then() to make use of the results retrieved and catch() to return the error message for when the promise fails.
Anywhere you have an asynchronous function with a callback, you should modify it to return a promise. Here's the updated version of the code with the callback hell problem.
console.log('Before')
getUser(1)
.then(user => getRepositories(user.gitHubUsername))
.then(repos => console.log('Hello')
.catch(err => console.log('Error', err.message))
console.log('After')
function getUser(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('Hello')
resolve({ id: id, gitHubUsername: 'Tandid'})
}, 2000)
})
}
function getRepositories(username){
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('Hello again')
resolve(['repo1', 'repo2'])
}, 2000)
})
}
You can call .then() multiple times as each promise is fulfilled and one .catch() to find errors within any of the executions.
To use multiple promises in parallel with one another, you can use promise.all(). The result will be returned as an array. If one of the promises fail, the final promise is rejected.
const p1 = new Promise(function(resolve, reject){
...
}
const p2 = new Promise(function(resolve, reject){
...
}
Promise.all([p1, p2])
.then(result => console.log(result) // 1
.catch(err => console.log('Error', err.message)
Promise.race() can be used to return the first fulfilled promise
Async/await
Helps you write asynchronous code like synchronous code. It's essentially syntactical sugar. Async and Await need to be used with one another
To catch errors, we need to use a try/catch block.
async function displayRepos() {
try {
const user = await getUser(1)
const repos = await getRepositories(user.gitHubUsername)
console.log(repos)
}
catch (err) {
console.log('Error', err.message')
}
}
displayRepos()
تعليقات