Back
PHONG PHAN
22/8/2024

Asynchronous processing with callbacks, promises, async/await

 Asynchronous processing with callbacks, promises, async/await

When programming JavaScript, you will often have to do time-consuming tasks such as: making requests to the server, retrieving data from the database, reading/writing files, etc. If you only do synchronous processing, it will definitely fail. very time consuming. To solve this problem, JavaScript provides a number of tools to help you handle asynchronously very well, such as using callbacks, promises or async/await. In this article, we will learn together what asynchronous processing is? Why do we need asynchronous processing? And ways to handle asynchronous processing in JavaScript. Please follow the article!

🥇The biggest misunderstanding when first learning JavaScript

We all know JavaScript is a programming language capable of asynchronous processing. So when first learning JavaScript, newbies often think that the code in JavaScript runs "messy", code A doesn't finish running until code B starts.

For example, in the code below, some people think console.log('Done') will run before the for loop completes, because it's asynchronous 😂.


But this is wrong, asynchrony in JavaScript only happens when we call an asynchronous function, such as setTimeout, fetch, axios, fs.readFile, fs.writeFile, ...

As for normal code like for, if, while, switch, ... still run sequentially as usual. We need to know which is asynchronous and which is synchronous, to know which code runs first and which code runs later. Often asynchronous code will have callbacks, promises, async/await. OK, it's that simple, but many of you don't understand 😁

🥇Callback

A callback is a function passed as an argument to another function and is executed after the completion of the previous function.

function fetchData(callback) {
    setTimeout(() => {
        console.log('Data fetched');
        callback(); // Execute the callback after fetching data
    }, 1000);
}

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

fetchData(processData); // `processData` is passed as a callback to `fetchData`

NODE:
Callbacks are often used in asynchronous processing, but note that callbacks can still be synchronous, it is not necessary that every callback is automatically asynchronous.

For example, the callback function passed to syncFunction is called a callback. It will be called immediately when syncFunction is called. As you can see, there is no asynchrony here at all.

function callback() {
  console.log('Hello World')
}
function syncFunction(cb) {
  cb()
}
syncFunction(callback)

🥇Promise

People have a saying "Javascript Promises are Eager and Not Lazy"
In the context of JavaScript and Promises, "eager" means that the Promise will be executed immediately when it is created, even before you call the then() method to process the result.

When you create a Promise, an asynchronous job is initiated. Promise will start doing that work immediately, even if you haven't used the then() method to process the result. This means that the Promise will start executing its work asynchronously and not wait for the then() call.
For example, in the following code:

const promise = new Promise((resolve, reject) => {
  console.log('Executing promise')
  resolve('Success')
})

promise.then((result) => {
  console.log('Promise resolved:', result)
})

console.log('Promise created')

First, when you create a Promise, the code in the constructor will be executed and the message "Executing promise" will be logged. Next, "Promise created" will be logged. Finally, when you call the then() method on Promise, the callback function in then() will be called and the message "Promise resolved: Success" will be logged.
If you want then() to be called then you must convert it into a function return promise. This is what we call lazy.

const promiseFunc = () =>
  new Promise((resolve, reject) => {
    console.log('Executing promise')
    resolve('Success')
  })

promiseFunc().then((result) => {
  console.log('Promise resolved:', result)
})

console.log('Promise created')


This way, when we call promiseFunc(), "Executing promise" will be logged. The result of the above code is:

Executing promise
Promise created
Promise resolved: Success

You can see it has no other code on it, but if you comment out the command line promiseFunc().then((result) => {... You will see that it no longer logs "Executing promise". As for the code above, it still logs "Executing promise" even if we don't call it promise.then((result) => {....


Ok, continuing below are short code snippets, can be considered cheatsheets, it will help you quickly practice promises.

Convert a callback to a promise

const getProduct = new Promise((resolve) => {
  // setTimeout will be called immediately
  setTimeout(() => {
    // The callback in here will run after 1 second
    console.log('setTimeout')
    resolve([])
  }, 1000)
})

Promise is a variable, not a function, this variable is an object. What we often see when calling an api like getApi().then() is that when calling getApi(), it returns a promise, not getApi which is a promise.

// Quickly create a promise that will resolve
const presolve = Promise.resolve(100)
// Quickly create a promise that will be rejected
const preject = Promise.reject(new Error('loi'))

Some functions that return promises are equivalent


// An async function will return a promise
// Even if the return value inside the function is not a promise
const handle = async () => {
  return 'hello'
}


// In case the return value is a promise
// Then everything remains unchanged, still as above
// There is no nesting like `handle().then(promise => promise.then(res => {console.log(res)}))`
const handle2 = async () => {
  return Promise.resolve('hello')
}
// This is also a function return promise similar to the above two cases
// The only difference is that it doesn't declare async
const handle3 = () => {
  return Promise.resolve('hello')
}

Handling promise chains In .then(), whatever we return will be the data for the next then. Even if you return a promise in then, it is the same as a normal return

handle3()
  .then((res) => {
    // These 2 returns are equivalent
    // return Promise.resolve(res + 'hi')
    return res + 'hi'
  })
  .then((res) => {
    console.log(res)
  })


Handle dependencies when using promises, for example function handle4 needs the result returned by function handle2

const handle4 = (value) => {
  return Promise.resolve('handle4 ' + value)
}

// ❌ callback hell, not recommended
// handle2().then((res) => {
// handle4(res).then((v) => {
// console.log(v)
// })
// })

// ✅ limit callback hell
handle2()
  .then((res) => {
    return handle4(res)
  })
  .then((v) => {
    console.log(v)
  })

A promise that is rejected will cause the application to crash. If you want to avoid crashes, you must always .catch it. catch without doing anything in it (for example without console.log), then when it fails, we don't know where the error is (invisible error). Once .catch is made, the chain behind is always promise.resolve, unless you throw or return Promise.reject in .catch

handle2()
  .then((res) => {
    throw new Error('error')
  })
  .catch((e) => {
    console.log('Definitely jumping in here')
  })
  .then((v) => {
    console.log('Jump here because there was a catch before, and the value v is undefined because catch doesn't return anything')
  })

🥇Async/Await


Async await helps us eliminate callbacks in promises. But you still can't escape the promise, they just inherit each other.
Important note:

  • You should only use async await for functions that handle promises inside
  • Can only await promises, not callbacks or regular functions
  • Async function is a function that returns a promise
const p = Promise.resolve('hello')

// If you want to use async await for the promise above, you must create a function
// Because async is only used for functions
// main is a function that returns a promise
const main = async () => {
  const data = await p
  console.log(data)
}
main()

Similarly, when using promises, you must add .catch(). When using async await, you must try catch, otherwise when it fails, it will crash the app.

const main = async () => {
  try {
    const data = await p
    console.log(data)
  } catch (error) {
    console.log(new Error(error))
  }
}
main()

When thrown in an async function, it will immediately cause that function to return a promise.reject()

const main = async () => {
  const data = await p
  console.log(data)
  throw new Error('Actively throw error')
}
main()

If there are many nested async functions, there is no need to try catch each async function, just try catch the outermost async function. Note: make sure to use await for the async function inside

const getApi1 = () => {
  return Promise.reject(new Error('API Error'))
}

const getApi2 = async () => {
  await getApi1()
}

const getApi3 = async () => {
  try {
    await getApi2()
  } catch (error) {
    // error handling here
  }
}

Similar to promises, once tried to catch, the function always returns a promise.resolve(x) unless you actively throw or return Promise.reject(x)

🥇Promise.all

Use promise.all when we want to run many promises in parallel, and only when all promises are resolved can we continue. Applied when we have many promises that do not depend on each other's results For example below, promise1, promise2, promise3 do not depend on each other's results, so we can use promise.all to run these 3 promises in parallel, instead of waiting for each one to finish running before running the next one.

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

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

const promise3 = () =>
  new Promise((resolve, reject) => {
    setTimeout(() => resolve('Promise 3'), 1500)
  })

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

// Or use async await
const main = async () => {
  try {
    const [result1, result2, result3] = await Promise.all([promise1(), promise2(), promise3()])
  } catch (error) {
    console.log('Error:', error)
  }
}

🥇Conclusion :

Above is some basic knowledge about asynchronous processing in JavaScript with callbacks, promises and async/await. This article does not write in detail about Promises or async/await, but only helps you see their basic usage, as well as their advantages compared to using regular callbacks. To know more about Promises and async/await, you can refer to the articles below: