Callbacks are one of the critical elements to understand JavaScript and Node.js. Nearly, all the asynchronous functions use a callback (or promises). In this post, we are going to cover callbacks in-depth and best practices.
This post assumes you know the difference between synchronous and asynchronous code.
JavaScript is an event-driven language. Instead of waiting for things to happen, it executes while listening for events. The way you respond to an event is using callbacks.
Related Posts:
JavaScript callbacks
A callback is a function that is passed as an argument to another function.
Callbacks are also known as higher-order function.
An example of a callback is the following:
1 | const compute = (n1, n2, callback) => callback(n1, n2); |
As you can see the function compute
takes two numbers and a callback function. This callback
function can be sum
, product
and any other that you develop that operates two numbers.
Callback Advantages
Callbacks can help to make your code more maintainable if you use them well. They will also help you to:
- Keep your code DRY (Do Not Repeat Yourself)
- Implement better abstraction where you can have more generic functions like
compute
that can handle all sorts of functionalities (e.g.,sum
,product
) - Improve code readability and maintainability.
So far, we have only seen callbacks that are executed immediately; however, most of the callbacks in JavaScript are tied to an event like a timer, API request or reading a file.
Asynchronous callbacks
An asynchronous callback is a function that is passed as an argument to another function and gets invoke zero or multiple times after certain events happens.
It’s like when your friends tell you to call them back when you arrive at the restaurant. You coming to the restaurant is the “event” that triggers the callback. Something similar happens in the programming world. The event could be you click a button, a file is loaded into memory, and request to a server API, and so on.
Let’s see an example with two callbacks:
1 | const id = setInterval(() => console.log('tick ⏰'), 1e3); |
First, you notice that we are using anonymous functions (in the previous example, we were passing the named functions such as sum
and product
). The callback passed to setInterval
is triggered every second, and it prints tick
. The second callback is called one after 5 seconds. It cancels the interval, so it just writes tick
five times.
Callbacks are a way to make sure a particular code doesn’t execute until another has already finished.
The console.log('tick')
only gets executed when a second has passed.
The functions setInterval
and setTimeout
callbacks are very simple. They don’t provide any parameters on the callback functions. But, if we are reading from the file system or network, we can get the response as a callback parameter.
Callback Parameters
The callback parameters allow you to get messages into your functions when they are available. Let’s say we are going to create a vanilla server on Node.js.
1 | const http = require('http'); |
We have two callbacks here. The http.createServer
‘s callback sends the parameters (req
)uest and (res
)ponse every time somebody connects to the server.
You can test this server using curl (or browser)
1 | curl 127.0.0.1:1777/this/is/cool |
There you have it! An HTTP server that replies to everyone that connects to it using a callback. But, What would happen if there’s an error? Let’s see how to handle that next.
Handling errors with Node.js callbacks
Some callbacks send errors on the first parameter and then the data (callback(error, data)
). That’s very common in Node.js API.
Let’s say we want to see all the directories on a given folder:
1 | const fs = require('fs'); |
As you notice, the first parameter will have an error message. If you run it, you would probably have the error message (unless you have the same name and directory).
1 | { [Error: ENOENT: no such file or directory, scandir '/Users/noAdrian/Code'] |
So that’s how you handle errors, you check for that parameter. But (there’s always a but) what if I need to do multiple async operations. The easiest way (but not the best) is to have a callback inside a callback:
1 | const fs = require('fs'); |
As you can see, this program will first read files in a directory and then check the file size of each file, and if it’s a directory, it will be omitted.
When callbacks are nested too many levels deep, we call this callback hell! 🔥 Or the pyramid of doom ⚠️
Because they are hard to maintain, how do we fix the callback hell? Read on!
Callback Hell problem and solutions
Callback hell is when you have too many nested callbacks.
1 | a(() => { |
To make your code better, you should:
- Keep you code shallow (avoid too many nested functions): keep your code at 1-3 indentation levels.
- Modularize: convert your anonymous callbacks into named functions.
- Use promises and async/await.
Let’s fix the callback hell from printFilesSize
keeping our code shallow and modularizing it.
1 | const fs = require('fs'); |
The original implement had five levels of indentation, now that we modularized it is 1-2 levels.
Callbacks are not the only way to deal with asynchronous code. In the following post we are going to cover:
- Promises
- Async/Await
- Generators
Stay tuned!
Related Posts: