jpcasa
12/21/2018 - 2:37 AM

JS Callback Functions

let name = () => 'Tom';
hello(name);

Technically speaking name is a callback function because it's passed to another function, but let's see what a callback function is in the context of an asynchronous operation.

In an async context, a callback function is just a normal JavaScript function that is called by JavaScript when an asynchronous operation is finished. By convention, established by Node, a callback function usually takes two arguments. The first captures errors, and the second captures the results. A callback function can be named or anonymous. Let’s look at a simple example showing how to read the contents of a file asynchronously using Node’s fs.readFile:

const fs = require('fs');

const handleReading = (err, content) => {
  if(err) throw new Error(err);
  return console.log(content);
};

fs.readFile('./my-file.txt', 'utf-8', handleReading);

The fs module has a method called readFile. It takes two required arguments, the first is the path to a file, and the last is a callback function. In the snippet above, the callback function is handleReading that takes two arguments. The first captures potential errors and the second captures the content.

Below is another example from the https module for making a GET request to a remote API server:

// code/callbacks/http-example.js
const https = require('https');
const url = 'https://jsonplaceholder.typicode.com/posts/1';

https.get(url, (response) => {
  response.setEncoding('utf-8');
  let body = '';
  response.on('data', (d) => {
    body += d;
  });
  response.on('end', (x) => {
    console.log(body);
  });
});

When you call the get method, a request is scheduled by JavaScript. When the result is available, JavaScript will call our function and will provide us with the result.

“Returning” an Async Result

When you perform an async operation, you cannot simply use the return statement to get the result. Let's say you have a function that wraps an async call. If you create a variable, and set it in the async callback, you won't be able to get the result from the outer function by simply returning the value:

function getData(options) {
  var finalResult;
  asyncTask(options, function(err, result) {
    finalResult = result;
  });
  return finalResult;
}

getData(); // -> returns undefined

In the snippet above, when you call getData, it is immediately executed and the returned value is undefined. That's because at the time of calling the function, finalResult is not set to anything. It's only after a later point in time that the value gets set. The correct way of wrapping an async call, is to pass the outer function a callback:

function getData(options, callback) {
  asyncTask(options, callback);
}

getData({}, function(err, result) {
  if(err) return console.log(err);
  return console.log(result);
});

In the snippet above, we define getData to accept a callback function as the second argument. We have also named it callback to make it clear that getData expects a callback function as its second argument.

Async Tasks In-order

If you have a couple of async tasks that depend on each other, you will have to call each task within the other task’s callback. For example, if you need to copy the content of a file, you would need to read the content of the file first before writing it to another file. Because of that you would need to call the writeFile method within the readFile callback:

const fs = require('fs');

fs.readFile('file.txt', 'utf-8', function readContent(err, content) {
  if(err) {
    return console.log(err);
  }
  fs.writeFile('copy.txt', content, function(err) {
    if(err) {
      return console.log(err);
    }
    return console.log('done');
  });
});

Now, it could get messy if you have a lot of async operations that depend on each other. In that case, it’s better to name each callback function and define them separately to avoid confusion:

const fs = require('fs');
fs.readFile('file.txt', 'utf-8', readCb);

function readCb(err, content) {
  if (err) {
    return console.log(err);
  }
  return fs.writeFile('copy.txt', content, writeCb);
}

function writeCb(err) {
  if(err) {
    return console.log(err);
  }
  return console.log('Done');
}

In the snippet above we have defined two callback functions separately, readCb and writeCb. The benefits might not be that obvious from the example above, but for operations that have multiple dependencies, the named callback functions can save you a lot of hair-pulling down the line.

Exercise: Async Callbacks in-order

In this exercise, we need to make a GET http call to an endpoint, and append the result to the content of a file and finally write the result to another one. For the sake of this exercise, let’s assume that each operation needs to happen in order:

  1. Make the GET http request to get the title of a post
  2. Read the content of a file
  3. Append the post title to the file content
  4. Write the result to a file

Solution

Below is a solution that uses named callbacks to perform each operation. In the Promises section we will see how to use promises to gather asyc results and take advantage of concurrent tasks. But for now, we are going to depend on each callback result to perform the next.

// code/callbacks/exercises/read-write/main.js
const fs = require('fs');
const request = require('request');
const url = 'https://jsonplaceholder.typicode.com/posts/2';

request.get(url, handleResponse);
function handleResponse(err, resp, body) {
  if(err) throw new Error;
  const post = JSON.parse(body);
  const title = post.title;
  fs.readFile('./file.txt', 'utf-8', readFile(title));
}

const readFile = title => (err, content) => {
  if(err) throw new Error(err);
  const result = title + content;
  fs.writeFile('./result.txt', result , writeResult);
}

function writeResult(err) {
  if(err) throw new Error(err);
  console.log('done');
}