The callback function is one of those concepts that every JavaScript developer should know. Callbacks are used in arrays, timer functions, promises, event handlers, and much more.
In this post, I will explain the concept of a callback function. Also, I'll help you distinguish the 2 types of callbacks: synchronous and asynchronous.
1. The callback function
How can you compose a message to greet a person?
Let's create a function greet(name)
that accepts a name
argument. The function should return the greeting message:
function greet(name) { return `Hello, ${name}!`;}greet('Cristina'); // => 'Hello, Cristina!'
What about greeting a list of persons? That's possible using a special array method array.map()
:
const persons = ['Cristina', 'Ana'];const messages = persons.map(greet);messages; // => ['Hello, Cristina!', 'Hello, Ana!']
persons.map(greet)
takes each item of the persons
array, and invokes the function greet()
using each item as an invocation argument: greet('Cristina')
, greet('Ana')
.
What's interesting is that persons.map(greet)
method accepts greet()
function as an argument. Doing so makes the greet()
a callback function.
The persons.map(greet)
is a function that accepts another function as an argument, so it is named a higher-order function.
The callback function is supplied as an argument to a higher-order function that invokes ("calls back") the callback function to perform an operation.
What's important is that the higher-order function takes the full responsibility of invoking the callback and supplying it with the right arguments.
In the previous example, the higher-order function persons.map(greet)
takes the responsibility to invoke the greet()
callback function with each item of the array as an argument: 'Cristina'
and 'Ana'
.
That brings to an easy rule for identifying callbacks. If you've defined a function and you're not invoking it by yourself — but rather supply as an argument to another function — then you've created a callback.
You can always write by yourself higher-order functions that use callbacks. For example, here's an equivalent version the array.map()
method:
function map(array, callback) { const mappedArray = []; for (const item of array) { mappedArray.push( callback(item) ); } return mappedArray;}function greet(name) { return `Hello, ${name}!`;}const persons = ['Cristina', 'Ana'];const messages = map(persons, greet);messages; // => ['Hello, Cristina!', 'Hello, Ana!']
map(array, callback)
is a higher-order function since it accepts a callback function as an argument, and then inside of its body invokes that callback function: callback(item)
.
Note that a regular function (defined using function
keyword) or an arrow function (defined using the fat arrow =>
) can equally serve as callbacks.
2. The synchronous callback
There are 2 types of callbacks by the way they're invoked: synchronous and asynchronous callbacks.
The synchronous callback is executed during the execution of the higher-order function that uses the callback.
In other words, the synchronous callbacks are blocking: the higher-order function doesn't complete its execution until the callback is done executing.
For example, recall the map()
and greet()
functions.
function map(array, callback) { console.log('map() starts'); const mappedArray = []; for (const item of array) { mappedArray.push(callback(item)) } console.log('map() completed'); return mappedArray;}function greet(name) { console.log('greet() called'); return `Hello, ${name}!`;}const persons = ['Cristina'];map(persons, greet);// logs 'map() starts'// logs 'greet() called'// logs 'map() completed'
greet()
is a synchronous callback because it's being executed at the same time as the higher-order function map()
. You can try the demo.
The synchronous way to invoke the callbacks:
- The higher-order function starts execution:
'map() starts'
- The callback function executes:
'greet() called'
- Finally, the higher-order function completes its execution:
'map() completed'
2.1 Examples of synchronous callbacks
A lot of methods of native JavaScript types use synchronous callbacks.
The most used ones are the array methods like array.map(callback)
, array.forEach(callback)
, array.find(callback)
, array.filter(callback)
, array.reduce(callback, init)
:
// Examples of synchronous callbacks on arraysconst persons = ['Ana', 'Elena'];persons.forEach( function callback(name) { console.log(name); });// logs 'Ana'// logs 'Elena'const nameStartingA = persons.find( function callback(name) { return name[0].toLowerCase() === 'a'; });nameStartingA; // => 'Ana'const countStartingA = persons.reduce( function callback(count, name) { const startsA = name[0].toLowerCase() === 'a'; return startsA ? count + 1 : count; }, 0);countStartingA; // => 1
string.replace(callback)
method of the string type also accepts a callback that is executed synchronously:
// Examples of synchronous callbacks on stringsconst person = 'Cristina';// Replace 'i' with '1'person.replace(/./g, function(char) { return char.toLowerCase() === 'i' ? '1' : char; }); // => 'Cr1st1na'
3. The asynchronous callback
The asynchronous callback is executed after the execution of the higher-order function.
Simply saying, the asynchronous callbacks are non-blocking: the higher-order function completes its execution without waiting for the callback. The higher-order function makes sure to execute the callback later on a certain event.
In the following example, the later()
function is executed with a delay of 2 seconds:
console.log('setTimeout() starts');setTimeout(function later() { console.log('later() called');}, 2000);console.log('setTimeout() completed');// logs 'setTimeout() starts'// logs 'setTimeout() completed'// logs 'later() called' (after 2 seconds)
later()
is an asynchronous callback because setTimeout(later, 2000)
starts and completes its execution, but later()
is executed after passing 2 seconds. Try the demo.
The asynchronous way to invoke the callbacks:
- The higher-order function starts execution:
'setTimeout() starts'
- The higher-order function completes its execution:
'setTimeout() completed'
- The callback function executes after 2 seconds:
'later() called'
3.1 Examples of asynchronous callbacks
The timer functions invoke the callbacks asynchronously:
setTimeout(function later() { console.log('2 seconds have passed!');}, 2000);// After 2 seconds logs '2 seconds have passed!' setInterval(function repeat() { console.log('Every 2 seconds');}, 2000);// Each 2 seconds logs 'Every 2 seconds!'
DOM event listeners also invoke the event handler function (a subtype of callback functions) asynchronously:
const myButton = document.getElementById('myButton');myButton.addEventListener('click', function handler() { console.log('Button clicked!');});// Logs 'Button clicked!' when the button is clicked
4. Asynchronous callback function vs asynchronous function
The special keyword async
placed before the function definition creates an asynchornous function:
async function fetchUserNames() { const resp = await fetch('https://api.github.com/users?per_page=5'); const users = await resp.json(); const names = users.map(({ login }) => login); console.log(names);}
fetchUserNames()
is asynchronous since it's prefixed with async
. The function fetches await fetch('https://api.github.com/users?per_page=5')
first 5 users from GitHub. Then extracts from the response object the JSON data: await resp.json()
.
The asynchronous functions are syntactic sugar on top of promises. When encountering the expression await <promise>
(note that calling fetch()
returns a promise), the asynchronous function pauses its execution until the promise is resolved.
An asynchronous callback function and an asynchronous function are different terms.
The asynchronous callback function is executed in a non-blocking manner by the higher-order function. But the asynchronous function pauses its execution while waiting for promises (await <promise>
) to resolve.
However... you can use an asynchronous function as an asynchronous callback!
Let's make the asynchornous function fetchUserNames()
an asynchronous callback called on button click:
const button = document.getElementById('fetchUsersButton');button.addEventListener('click', fetchUserNames);
Open the demo and click Fetch Users. When the request completes, you'll see a list of users logged to the console.
5. Summary
The callback is a function that's accepted as an argument and executed by another function (the higher-order function).
There are 2 kinds of callback functions: synchronous and asynchronous.
The synchronous callbacks are executed at the same time as the higher-order function that uses the callback. Synchronous callbacks are blocking.
On the other side, the asynchronous callbacks are executed at a later time than the higher-order function. Asynchronous callbacks are non-blocking.
Quiz: does setTimeout(callback, 0)
execute the callback
synchronously or asynchronously?