Advanced React

Callback Hell and Async

Callback Hell

Callbacks are just the name of a convention for using Javascript functions. When you call an Asynchronous (async) function, which needs some time to produce a result, and you want to get the result from this calling, you will need to pass a callback to the function. Callbacks are usually passed in as the last argument of the Async function. When the result is calculated, the function will call the callback to return back the result. That's why they're called Callbacks.

function imageProcessing(img, callback) {
  const result = null;

  // processing image...
  
  callback(result)
}

const imgFile = ...

imageProcessing(imgFile, function(result) {
  // result from image processing
  console.log(result)
});

Callback Hell is when you have multiple Async functions and they need to wait for others to complete before they can run. This will result in calling an Async function inside another Async function callback. This chain will continue until all the Async functions have been called, creating a multi-level of nested callbacks, which is called Callbacks Hell. Look at the example below:

fs.readdir(source, function (err, files) {
  if (err) {
    console.log('Error finding files: ' + err)
  } else {
    files.forEach(function (filename, fileIndex) {
      console.log(filename)
      gm(source + filename).size(function (err, values) {
        if (err) {
          console.log('Error identifying file size: ' + err)
        } else {
          console.log(filename + ' : ' + values)
          aspect = (values.width / values.height)
          widths.forEach(function (width, widthIndex) {
            height = Math.round(width / aspect)
            console.log('resizing ' + filename + 'to ' + height + 'x' + height)
            this.resize(width, height).write(dest + 'w' + width + '_' + filename, function(err) {
              if (err) console.log('Error writing file: ' + err)
            })
          }.bind(this))
        }
      })
    })
  }
})

How to fix Callback Hell?

Keep code shallow

A simple solution without changing too much in your code is keeping your code shallow by naming the callbacks so we can flatten them out. See the example below:

var form = document.querySelector('form')
form.onsubmit = function (submitEvent) {
  var name = document.querySelector('input').value
  request({
    uri: "http://example.com/upload",
    body: name,
    method: "POST"
  }, function (err, response, body) {
    var statusMessage = document.querySelector('.status')
    if (err) return statusMessage.value = err
    statusMessage.value = body
  })
}

can be rewritten like this:

document.querySelector('form').onsubmit = formSubmit

function formSubmit (submitEvent) {
  var name = document.querySelector('input').value
  request({
    uri: "http://example.com/upload",
    body: name,
    method: "POST"
  }, postResponse)
}

function postResponse (err, response, body) {
  var statusMessage = document.querySelector('.status')
  if (err) return statusMessage.value = err
  statusMessage.value = body
}

Using Promise

Mostly each built-in function or utility nowadays have a promised version of it, so you can make use of it to prevent Callback Hell. Check out the following React example

function Uploader() {
  const inputChange = (e) => {
    const file = e.target.files[0]
    loadingFile(file, (img) => {
      getSignedUrlToUploadFile(img, (url) => {
        uploadImgToUrl(img, url, (imageUrl) => {
          setImageUrl(imageUrl)
        })
      })
    })
  }
  
  return (
    <div>
      <input type="file" onChange={inputChange}>
    </div>
  )
}

You can rewrite the code with Promise like this

function Uploader() {
  const inputChange = (e) => {
    const file = e.target.files[0]
    loadingFile(file)
      .then((img) => getSignedUrlToUploadFile(img))
      .then((img, url) => uploadImgToUrl(img, url))
      .then((imageUrl) => setImageUrl(imageUrl))
  }
  
  return (
    <div>
      <input type="file" onChange={inputChange}>
    </div>
  )
}

Promise Hell

Promise only worked if you use it correctly. Be careful or you will create another version of Callback Hell called Promise Hell

const inputChange = (e) => {
  const file = e.target.files[0]
  loadingFile(file).then((img) => {
    getSignedUrlToUploadFile(img).then((url) => {
      uploadImgToUrl(img, url).then(imageUrl => {
        setImageUrl(imageUrl)
      })
    })
  })
}

Async/await to the rescue

From the last section, we knew that Promise can be used to prevent Callback Hell but it's not the optimal way, and also you can not use the previous Promise results too far away. To best solve the problem, Async/await syntax was created and added in ES7 (ECMAScript 7 / ES2015)

First, let's take a look at an example of Async/await syntax

function resolveAfter2Seconds() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve('resolved');
    }, 2000);
  });
}

async function asyncCall() {
  console.log('calling');
  const result = await resolveAfter2Seconds();
  console.log(result);
  // expected output: "resolved"
}

asyncCall();

asyncCall is an async function that is declared with the async keyword, and the await keyword is permitted within it. The async and await keywords enable asynchronous, promise-based behavior to be written in a cleaner style, avoiding the need to explicitly configure promise chains.

As you can see in the example, the best thing about this syntax is that you can call async functions or Promises just like the way you do with normal synchronous functions by using only the keyword await in front. To call with an await prefix keyword, the functions must be async functions, Promises or a function that return a Promise like the example above. And await can only be used inside a function declared with the async keyword.

Using async/await syntax, the React upload file example can be rewritten in a much cleaner way like this:

function Uploader() {
  const inputChange = async(e) => {
    const file = e.target.files[0]
    const img = await loadingFile(file);
    const url = await getSignedUrlToUploadFile(img);
    const imageUrl = await uploadImgToUrl(img, url);
    setImageUrl(imageUrl)
  }
  
  return (
    <div>
      <input type="file" onChange={inputChange}>
    </div>
  )
}

You may ask Promises have .catch() methods for catching errors, then what about async/await. Another cool thing when using the new syntax is that it enables the use of ordinary try / catch blocks around asynchronous code, so you can use it to catch async function errors. Check the example below

const inputChange = async(e) => {
  try {
    const file = e.target.files[0]
    const img = await loadingFile(file);
    const url = await getSignedUrlToUploadFile(img);
    const imageUrl = await uploadImgToUrl(img, url);
    setImageUrl(imageUrl)
  } catch (err) {
    console.log('There is an error when uploading image', err)
  }
}
Previous
Lazy Loading / Suspense