Callback Functions

Before talking about callback functions in an async context, it's important to learn how functions can be passed to other functions. Let's look at an example and see how functions can be passed around just like any value in JavaScript.

var name = 'Tom';
hello(name);

In the code snippet above we define a variable called name and we assign a string to it. Then we pass it to the hello function as an argument. We can do the exact same thing with a function. We can define name to be a function instead and pass it to hello:

function name() {
  return '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, 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, but it's better to name them. Let's look at a simple example showing how to read the content of a file asynchronously using Node's fs.readFile method:

function handleReading(error, result) {
  console.log(result);
}
fs.readFile('./my-file.txt', handleReading);

The fs module has a method called readFile. It takes two required arguments, the first is the path to the file, and the second 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, function(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);
  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: Simple Callback

Define a compute function that takes two arguments:

  1. An array of integers
  2. A callback function that operates on the passed array

For example, the following would return 6 (assuming that the addAll function is defined).

const result = compute([1,2,3], addAll);

Solution

One possible solution is to check if the first argument is an array of integers and then call the callback function with the args:

code/callbacks/exercises/simple-callback.js

function compute(nums, fn) {
  if(!Array.isArray(nums)) return NaN;
  const isAnyNotInteger = nums.some(v => !Number.isInteger(v));
  if(isAnyNotInteger) return NaN;
  return fn(nums);
}

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 possible solution that uses named callbacks to perform each operation. In the Promises chapter 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');
}

results matching ""

    No results matching ""