A promise is an object that represents the eventual completion (or failure) of an asynchronous operation and its resulting value. Promises provide a cleaner and more flexible way to handle asynchronous operations compared to callbacks. They allow you to chain multiple asynchronous operations together and handle errors more effectively.
The general syntaxt of a promise is as follows:
const promise = new Promise((resolve, reject) => {
// Perform an asynchronous operation
// If the operation is successful, call resolve with the result
// If the operation fails, call reject with an error
});
promise.then((result) => {
// Handle the successful completion of the promise
}).catch((error) => {
// Handle any errors that occur during the promise
});
The Promise
constructor takes a function as an argument, which in turn takes two functions: resolve
and reject
. The resolve
function is called when the asynchronous operation is successful, and the reject
function is called when the operation fails.
Once the promise is created, you can use the then
method to handle the successful completion of the promise and the catch
method to handle any errors that occur during the promise.
You can also chain multiple promises together using the then
method. This allows you to perform a series of asynchronous operations in sequence.
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Promise 1 resolved");
}, 2000);
});
const promise2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Promise 2 resolved");
}, 1000);
});
promise1
.then((result) => {
console.log(result);
return promise2;
})
.then((result) => {
console.log(result);
});
Let’s refactor the example from the previous section to use promises instead of callbacks. We start with the getId
function, which currently uses a callback to handle the asynchronous operation of fetching the student’s ID:
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", (id) => {
console.log("John Doe ID:", id);
});
console.log("Still listening to events!");
We need to update the getId
function to return a promise instead of using a callback.
function getId(student) {
const promise = new Promise();
return promise;
}
Notice that we have removed the callbackFunction
parameter. Moreover, we have created a new Promise
object and returned it from the function. The Promise
constructor takes a function as an argument, which in turn takes two functions: resolve
and reject
.
function getId(student) {
const promise = new Promise((resolve, reject) => {
});
return promise;
}
We call the resolve
function when the asynchronous operation is successful and the reject
function when the operation fails.
function getId(student) {
const promise = new Promise((resolve, reject) => {
let success;
// do async work
if (success) {
resolve();
} else {
reject();
}
});
return promise;
}
In this case, when the asynchronous operation is successful, we want to resolve the promise with the student’s ID. And when the operation fails, we want to reject the promise with an error. Let’s update the getId
function to reflect this:
function getId(student) {
const promise = new Promise((resolve, reject) => {
let success, id;
// do async work
console.log(`Fetching ${student} info!`);
if (success) {
console.log(`Received ${student} info!`);
resolve(id);
} else {
reject(new Error(`Unable to fetch ${student} info`));
}
});
return promise;
}
We can simulate the asynchronous operation using a setTimeout
function.
function getId(student) {
const promise = new Promise((resolve, reject) => {
let success, id;
console.log(`Fetching ${student} info!`);
setTimeout(() => {
id = "jdoe23";
success = true;
if (success) {
console.log(`Received ${student} info!`);
resolve(id);
} else {
reject(new Error(`Unable to fetch ${student} info`));
}
}, 5000);
});
return promise;
}
Here is a slightly more concise version of the getId
function:
function getId(student) {
return new Promise((resolve, reject) => {
let success, id;
console.log(`Fetching ${student} info!`);
setTimeout(() => {
id = "jdoe23";
success = true;
if (success) {
console.log(`Received ${student} info!`);
resolve(id);
} else {
reject(new Error(`Unable to fetch ${student} info`));
}
}, 5000);
});
}
We can now use the getId
function with promises to fetch the student’s ID:
console.log("Listening to events!");
getId("John Doe")
.then((id) => {
console.log("John Doe ID:", id);
})
.catch((error) => {
console.error(error);
});
console.log("Still listening to events!");
The then
method is used to handle the successful completion of the promise, and the catch
method is used to handle any errors that occur during the promise. Run the code above to see the output in the console.
Listening to events!
Fetching John Doe info!
Still listening to events!
Received John Doe info!
John Doe ID: jdoe23
Then, try changing the success
variable to false
in the getId
function to simulate a failed asynchronous operation. You should see the error message being logged to the console.
Listening to events!
Fetching John Doe info!
Still listening to events!
Error: Unable to fetch John Doe info
Let’s go ahead and refactor the getCourses
function to use promises as well. Here is the original version of the getCourses
function that uses a callback:
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);
}
We need to update the getCourses
function to return a promise instead of using a callback.
function getCourses(student_id) {
return new Promise((resolve, reject) => {
let success, courses;
console.log(`Fetching ${student_id}'s courses!`);
setTimeout(() => {
success = true;
courses = ["course-1", "course-2"];
console.log(`Received ${student_id}'s courses!`);
if (success) {
resolve(courses);
} else {
reject(new Error(`Unable to fetch ${student_id}'s courses`));
}
}, 5000);
});
}
We can now use the getCourses
function with promises to fetch the student’s courses:
console.log("Listening to events!");
getId("John Doe")
.then((id) => {
console.log("John Doe ID:", id);
return getCourses(id);
})
.then((courses) => {
console.log("John Doe Courses:", courses);
})
.catch((error) => {
console.error(error);
});
console.log("Still listening to events!");
Notice the first then
block where we call the getCourses
function after successfully fetching the student’s ID. We return the result of the getCourses
function to chain it with the next then
block. This is how we can chain multiple promises together using the then
method. This allows us to perform a series of asynchronous operations in sequence. Run the code above to see the output in the console.
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' ]
Let us refactor the getGrades
function to use promises as well. Here is the original version of the getGrades
function that uses a callback:
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);
}
We need to update the getGrades
function to return a promise instead of using a callback.
function getGrades(student_id, student_courses) {
return new Promise((resolve, reject) => {
let success, grades;
console.log(`Fetching ${student_id}'s grades!`);
setTimeout(() => {
success = true;
grades = student_courses.map((course) => {
return { course: course, grade: Math.floor(Math.random() * 100) };
});
console.log(`Received ${student_id}'s grades!`);
if (success) {
resolve(grades);
} else {
reject(new Error(`Unable to fetch ${student_id}'s grades`));
}
}, 5000);
});
}
We can now use the getGrades
function with promises to fetch the student’s grades:
console.log("Listening to events!");
getId("John Doe")
.then((id) => {
console.log("John Doe ID:", id);
return getCourses(id);
})
.then((courses) => {
console.log("John Doe Courses:", courses);
return getGrades(id, courses);
})
.then((grades) => {
console.log("John Doe Grades:", grades);
})
.catch((error) => {
console.error(error);
});
console.log("Still listening to events!");
Run the code above to see the output in the console.
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' ]
ReferenceError: id is not defined
So what happened here? The id
variable is not defined in the getGrades
function because it is out of scope. One way to fix this is to pass the id
variable along with the courses
variable when resolving the promise in the getCourses
function. This way, the id
variable will be available in the subsequent then
block. Here is the updated code:
function getCourses(student_id) {
return new Promise((resolve, reject) => {
let success, courses;
console.log(`Fetching ${student_id}'s courses!`);
setTimeout(() => {
success = true;
courses = ["course-1", "course-2"];
console.log(`Received ${student_id}'s courses!`);
if (success) {
- resolve(courses);
+ resolve({ student_id, courses });
} else {
reject(new Error(`Unable to fetch ${student_id}'s courses`));
}
}, 5000);
});
}
Notice how we are now resolving the promise with an object that contains both the student_id
and courses
. This allows us to access the student_id
in the subsequent then
block. Let’s update the then
block in the main code to reflect this change:
console.log("Listening to events!");
getId("John Doe")
.then((id) => {
console.log("John Doe ID:", id);
return getCourses(id);
})
- .then((courses) => {
+ .then(({ id, courses }) => {
console.log("John Doe Courses:", courses);
return getGrades(id, courses); // Now the id is available to pass to getGrades
})
.then((grades) => {
console.log("John Doe Grades:", grades);
})
.catch((error) => {
console.error(error);
});
console.log("Still listening to events!");
Notice how we are now destructuring the object returned by the getCourses
function to access both the id
and courses
variables. This allows us to pass the id
variable to the getGrades
function. Run the code above to see the output in the console.
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 undefined's grades!
Received undefined's grades!
John Doe Grades: [
{ course: 'course-1', grade: 99 },
{ course: 'course-2', grade: 57 }
]
You might eb wondering why we passed an object to the resolve
function instead of doing something like resolve(id, courses)
. The reason is that the resolve
function can only accept a single argument. By passing an object, we can include multiple values in a single argument and then destructure them in the then
block. This is a common pattern when working with promises.
At this point we have refactored the getId
, getCourses
, and getGrades
functions to use promises instead of callbacks.
Promises provide a cleaner way to chain multiple asynchronous operations together and handle errors more effectively. Promise chaining flattens the nested structure of callback hell. This makes the code more readable and maintainable.
There is more to promises than what we have covered here. Promises also have other methods like all
, race
, and allSettled
that can be used to work with multiple promises simultaneously. These are beyond the scope of this note, but you can explore them further in the MDN Web Docs.