A callback is a function that is passed as an argument to another function and is executed after the completion of an asynchronous operation. Callbacks are a common way to handle asynchronous operations in JavaScript. They allow you to define what should happen after an operation completes without blocking the main thread.
To explore callbacks, let’s consider a scenario where we need to fetch data from a server and process it. We will use a simple example to demonstrate how callbacks can be used to handle asynchronous operations.
In the following code snippet, we have three functions: getId
, getCourses
, and getGrades
. Each function represents a step in the process of fetching data for a student. The getId
function fetches the student’s ID from the database, the getCourses
function fetches the student’s courses, and the getGrades
function fetches the student’s grades.
function getId(student) {
}
function getCourses(student_id) {
}
function getGrades(student_id, student_courses) {
}
console.log("Listening to events!");
const student_id = getId("John Doe");
const student_courses = getCourses(student_id);
const student_grades = getGrades(student_id, student_courses);
console.log(student_grades);
console.log("Still listening to events!");
The code above demonstrates a synchronous approach to fetching data for a student. A naive implementation might look like this:
function getId(student) {
console.log(`Fetching ${student} info!`);
// read from database!
const id = "jdoe23";
console.log(`Received ${student} info!`);
return id;
}
console.log("Listening to events!");
const student_id = getId("John Doe");
console.log("John Doe ID:", student_id);
console.log("Still listening to events!");
However, in a real-world scenario, fetching data from a server is an asynchronous operation that takes time to complete. Let’s simulate this by adding a delay to the getId
function:
function getId(student) {
console.log(`Fetching ${student} info!`);
// read from database!
let id;
setTimeout(() => {
id = "jdoe23";
console.log(`Received ${student} info!`);
}, 5000);
return id; // <- why is this undefined?
}
console.log("Listening to events!");
const student_id = getId("John Doe");
console.log("John Doe ID:", student_id);
console.log("Still listening to events!");
If you run the code above, you will notice that the student_id
is undefined
. This is because the getId
function returns id
before the asynchronous operation completes. The setTimeout
function schedules the assignment of id
to “jdoe23” after 5000 milliseconds, but the getId
function returns id
immediately after the setTimeout
function is called.
Let’s try another approach by returning the id
value inside the setTimeout
function:
function getId(student) {
console.log(`Fetching ${student} info!`);
// read from database!
let id;
setTimeout(() => {
id = "jdoe23";
console.log(`Received ${student} info!`);
return id; // <- where is this value returned to?
}, 5000);
}
console.log("Listening to events!");
const student_id = getId("John Doe");
console.log("John Doe ID:", student_id);
console.log("Still listening to events!");
If you run the code above, you will notice that the student_id
remains undefined
. This is because the return id
statement inside the setTimeout
function does not return the value to the getId
function. The setTimeout
function is executed asynchronously, and the return id
statement is executed in a different context.
Now, let us use a callback function to handle the asynchronous operation:
function getId(student, callbackFunction) {
console.log(`Fetching ${student} info!`);
// read from database!
let id;
setTimeout(() => {
id = "jdoe23";
console.log(`Received ${student} info!`);
callbackFunction(id);
}, 5000);
}
console.log("Listening to events!");
getId("John Doe", (student_id) => {
console.log("John Doe ID:", student_id);
});
console.log("Still listening to events!");
In the code above, we modified the getId
function to accept a callbackFunction
as an argument. The callbackFunction
is executed after the asynchronous operation completes. We pass the id
value to the callbackFunction
inside the setTimeout
function. This allows us to handle the asynchronous operation in a non-blocking way.
When you run the code above, you will see the following output:
Listening to events!
Fetching John Doe info!
Received John Doe info!
Still listening to events!
Received John Doe info!
John Doe ID: jdoe23
Let’s now apply the same approach to the getCourses
function:
function getCourses(student_id, callbackFunction) {
console.log(`Fetching ${student_id}'s courses!`);
let courses;
setTimeout(() => {
courses = ["course-1", "course-2"];
console.log(`Received ${student_id}'s courses!`);
callbackFunction(courses);
}, 5000);
}
console.log("Listening to events!");
getId("John Doe", (student_id) => {
console.log("John Doe ID:", student_id);
getCourses(student_id, (courses) => {
console.log("John Doe Courses:", courses);
});
});
console.log("Still listening to events!");
When you run the code above, you will see the following output:
Listening to events!
Fetching John Doe info!
Still listening to events!
Received John Doe info!
John Doe ID: jdoe23
Fetching jdoe23's courses!
Received jdoe23's courses!
John Doe Courses: [ 'course-1', 'course-2' ]
Finally, let’s apply the same approach to the getGrades
function:
function getGrades(student_id, student_courses, callbackFunction) {
console.log(`Fetching ${student_id}'s grades!`);
let grades;
setTimeout(() => {
grades = student_courses.map(course => {
return { course: course, grade: Math.floor(Math.random() * 100) };
});
console.log(`Received ${student_id}'s grades!`);
callbackFunction(grades);
}, 5000);
}
console.log("Listening to events!");
getId("John Doe", (id) => {
console.log("John Doe ID:", id);
getCourses(id, (courses) => {
console.log("John Doe Courses:", courses);
getGrades(id, courses, (grades) => {
console.log("John Doe Grades:", grades);
});
});
});
console.log("Still listening to events!");
When you run the code above, you will see the following output:
Listening to events!
Fetching John Doe info!
Still listening to events!
Received John Doe info!
John Doe ID: jdoe23
Fetching jdoe23's courses!
Received jdoe23's courses!
John Doe Courses: [ 'course-1', 'course-2' ]
Fetching jdoe23's grades!
Received jdoe23's grades!
John Doe Grades: [
{ course: 'course-1', grade: 42 },
{ course: 'course-2', grade: 89 }
]
In this example, we used callbacks to handle asynchronous operations in a non-blocking way. We passed callback functions to the getId
, getCourses
, and getGrades
functions to handle the results of the asynchronous operations. This allowed us to fetch data for a student in a sequential manner without blocking the main thread.
One of the challenges of using callbacks for handling asynchronous operations is the issue of callback hell. Callback hell occurs when you have multiple nested callbacks, making the code difficult to read and maintain. This can happen when you have multiple asynchronous operations that depend on each other.
method1(arg1, (arg2) => {
method2(arg2, (arg3) => {
method3(arg3, (arg4) => {
method4(arg4, (arg5) => {
method5(arg5, (arg6) => {
method6(arg6, (arg7) => {
// ...
// CALLBACK HELL (or the Christmas tree problem)
// ...
});
});
});
});
});
});
To avoid callback hell, you can use Promises or async/await, which provide a cleaner and more readable way to handle asynchronous operations.