Felix Kling

JavaScript: How (not) to get a value "out of" a callback

Callback functions appear to be a complex topic for people, especially if they are new to JavaScript. I believe however that they are often making it more difficult for themselves than it has to be.

Something is complex when there are many rules and/or exceptions around it.
Remembering all the rules and exceptions can be difficult. If you assume that there are special rules around callback functions then you perceive them as more complex or difficult to understand.

In this post I’m showing two common mistakes with callbacks and try to show that callback functions are functions like any other with no extra rules. Only a basic understanding of functions is required to understand callbacks, although we will briefly touch on asynchronous code at the end.

Function basics #

Just like in mathematics, a function processes some input and returns a result (output). Here is a simple function that adds two numbers:

function add(a, b) {
  return a + b;
}

The function has two parameters, a and b and returns their sum. When we call the function as

var result = add(1,2);
// result = 3

we are supplying two arguments, which are assigned to each parameter respectively, and store the function’s return value in a variable.

However, functions do not have to accept input or return a result, they can also “just do” something:

// No return value
function logWithTime(message) {
  console.log(Date.now() + " " + message);
}
logWithTime("Hello World!");

// No input or return value
function warn() {
  alert("This is a warning!");
}
warn();

While both examples have a visible outcome, they are not returning a value.

Who is the caller? #

Note that in all examples so far, we directly called the functions, which allowed us to pass arguments (if they accepted them) and do something with their return values (if they returned something).

Most functions however are not directly called by us but by other functions:

function stringRepeat(str, count) {
  var result = "";
  while (count > 0) {
    result += str;
    count--;
  }
  return result;
}

function leftPad(str, width, fillCharacter) {
  if (str.length >= width) {
    return str;
  }
  return stringRepeat(fillCharacter, width - str.length) + str;
}

var result = leftPad("foo", 5, "x");
// result = "xxfoo"

Here we are only calling leftPad directly, which in turn calls stringRepeat. This makes leftPad the caller of stringRepeat, which means it controls which arguments to pass and what to do with the return value, not us.

Execution flow #

This might be obvious, but just to reaffirm: Statements are evaluated in the order they are written in source code.

If two function calls follow each other, the second one will only be executed after the first function call returned:

foo();
bar();

bar is only called after foo returned, which implies that all statements inside foo are evaluated before bar is called.

What are callbacks? #

Simply put a callback is a function that is passed as argument to another function. Why is that useful? Lets look at the following example:

function allUpper(array) {
  var result = [];
  for (var i = 0; i < array.length; i++) {
    result[i] = array[i].toUpperCase();
  }
  return result;
}

function allLower(array) {
  var result = [];
  for (var i = 0; i < array.length; i++) {
    result[i] = array[i].toLowerCase();
  }
  return result;
}

allUpper(['Foo', 'Bar']); // ['FOO', 'BAR']
allLower(['Foo', 'Bar']); // ['foo', 'bar']

Both functions do basically the same thing: They process each element in an array and return a new array. It would be great if we can somehow separate the result array creation from the element processing:

function map(array, processElement) {
  var result = [];
  for (var i = 0; i < array.length; i++) {
    result[i] = processElement(array[i]);
  }
  return result;
}

This function has two parameters: The first is expected to be an array and the second one is expected to be a function. That function is passed a single array element and should return the processed result.

We can use this function like so:

function toUpper(item) {
  return item.toUpperCase();
}

function toLower(item) {
  return item.toLowerCase();
}

map(['Foo', 'Bar'], toUpper); // ['FOO', 'BAR']
map(['Foo', 'Bar'], toLower); // ['foo', 'bar']

Here toUpper and toLower are used as callbacks for map.

(Processing each element in an array and producing a new array is such a common operation that it is built into the language: Array#map).

IMPORTANT: The caller of the callback is usually the function we pass the callback to, but never us.


Misconception 1: Returning from the callback #

// BROKEN!
var someData = someFunction(function(someResult) {
  return someResult;
});
console.log(someData); // expected to be equal to `someResult`

The first misconception is that returning from the callback will somehow return from the function the callback is passed to. Maybe it’s because the function definition is inlined with the function call and thus the callback somehow appears to be “inside” the other function.

However, the code behaves exactly the same if we defined the callback upfront:

// BROKEN!
function doSomething(someResult) {
  return someResult;
}

var someData = someFunction(doSomething);
console.log(someData); // expected to be `someResult`

Even without knowing anything about someFunction, we can make the following observations:

This is the key point: We do not call doSomething, therefore we don’t have any control over what happens to its return value.

It could indeed be the case that someFunction returns doSomething’s return value, e.g.

function someFunction(callback) {
  return callback(42);
}

and the example above would work.

But it also may not:

function someFunction(callback) {
  callback(42);
}

We cannot know just by looking at the initial example. What the return value of someFunction is really depends on what the function does (which is usually described in its documentation) and is not necessarily related to the return value of the callback.


Misconception 2: Assigning to an “outer” variable #

// BROKEN!
var someData;
someFunction(function(someResult) {
  someData = someResult;
});
console.log(someData);

In this case the callback explicitly assigns the value it gets passed to another variable, defined outside of it.

The same code with a separate function declaration:

// BROKEN!
var someData;

function doSomething(someResult) {
  someData = someResult;
}

someFunction(doSomething);
console.log(someData);

Without knowing what someFunction does, we can state the following:

If someFunction calls doSomething before someFunction returns, someResult is assigned to someData before console.log is called. In other words, console.log(someData) will log the same value as someResult.

How can a function call another function after it returns? That’s were asynchronous code execution comes into play.

A short introduction to asynchronous code #

When we talk about asynchronous code, we usually refer to a function that will be executed some time in the future, either in response to some event or because a specific amount of time has passed.

A simple way to schedule a function to run in the future is via setTimeout:

setTimeout(function() { console.log('hello'); }, 1000);

This schedules the passed function to be called “later” and after at least one second.

The second important aspect about asynchronous code execution is that scheduled functions are only executed if the runtime isn’t busy executing other code.

Consider the following example:

setTimeout(function() { console.log('world'); }, 0);
console.log('hello');

setTimeout schedules a function to be called later, but as soon as possible (the timeout is 0). However the callback won’t be executed immediately because the runtime is still busy evaluating the function call that follows the setTimeout call, console.log('hello'). Only after that call the runtime is free to process any scheduled functions.

This is also called run-to-completion.

“Wait!” you might think. “Didn’t you say earlier that statements are executed in the order they are written? Why is the function passed to setTimeout not called before the second console.log?”

As in the previous misconception: We are not calling the function passed to setTimeout. We merely pass the function object itself. setTimeout isn’t calling the function either. It’s telling the runtime to call this function some time in the future.

Back to the example:

// BROKEN!
var someData;

function doSomething(someResult) {
  someData = someResult;
}

someFunction(doSomething);
console.log(someData);

If someFunction doesn’t call doSomething immediately but schedules it to run some time in the future, console.log(someData) is guaranteed to execute before the assignment someData = someResult happens.

Here again, the documentation of someFunction should describe how the callback is used and whether the function performs asynchronous computation or not.


Conclusion #

A simple thought experiment to spot issues with callbacks is the following:

Consider one of the examples again:

// BROKEN!
var someData;
someFunction(function(someResult) {
  someData = someResult;
});
console.log(someData);

We are trying to get the result “out” that someFunction passed to the callback. If someFunction returned the result instead we could simplify our code to:

var someData = someFunction();
console.log(someData);

But the very fact that someFunction requires a callback instead of directly returning the result is already an indication that assigning the result to someData won’t work!

So, what should you do?

Instead of trying to get a value to the code that needs it, move the code to where the value is.

That means moving the code inside the callback function or into another function that is called from the callback.

For example:

// BROKEN!
var someData;

function doSomething(someResult) {
  someData = someResult;
}

someFunction(doSomething);

// Code that needs someData
console.log(someData);

// Other code

becomes

// Working

function doSomething(someData) {
  // Code that needs someData
  console.log(someData);
}

someFunction(doSomething);

// Other code

or

// Working

function doSomethingElse(someData) {
  // Code that needs someData
  console.log(someData);
}

function doSomething(someResult) {
  // do stuff
  doSomethingElse(someResult);
}

someFunction(doSomething);

// Other code

Further reading #