In the previous lesson we looked at callback functions and the "callback hell" problem that can arise with complex asynchronous operations. Now let's learn about a more modern solution — Promise objects, which provide a more elegant way to handle asynchronicity in JavaScript.
A Promise is an object representing the eventual completion (or failure) of an asynchronous operation and its resulting value. You can think of it as a "promise" to deliver a certain result in the future.
In the context of Jurassic Park, a Promise is like sending a dinosaur DNA sample to the laboratory for analysis. The laboratory does not return results immediately — instead it gives you a ticket number representing a "promise" to deliver results when the analysis is complete.
A Promise can be in one of three states:
Creating a new Promise in JavaScript looks like this:
1const dnaAnalysisPromise = new Promise((resolve, reject) => {
2 // Simulating dinosaur DNA analysis (takes 3 seconds)
3 console.log("Analyzing dinosaur DNA sample...");
4
5 setTimeout(() => {
6 const successRate = Math.random();
7
8 if (successRate > 0.2) {
9 // Analysis completed successfully
10 const results = {
11 species: "Velociraptor",
12 age: "Approximately 68 million years",
13 diet: "Carnivore",
14 geneticMarkers: ["ACGT-237", "TGGC-118", "GACT-339"]
15 };
16
17 resolve(results); // Fulfilling the promise
18 } else {
19 // Analysis failed
20 reject(new Error("DNA sample damaged or insufficient")); // Rejecting the promise
21 }
22 }, 3000);
23});In the example above:
resolve and rejectsetTimeout)resolve(result) to fulfill the promisereject(reason) to reject the promiseTo handle the result of a Promise, we use the
then(), catch(), and finally() methods:1console.log("Sending sample to the laboratory...");
2
3dnaAnalysisPromise
4 .then(results => {
5 console.log("DNA analysis completed successfully!");
6 console.log(`Species: ${results.species}`);
7 console.log(`Age: ${results.age}`);
8 console.log(`Diet: ${results.diet}`);
9 console.log("Genetic markers:", results.geneticMarkers.join(", "));
10 return results; // We can return a value that will be passed to the next .then()
11 })
12 .catch(error => {
13 console.error("DNA analysis error:", error.message);
14 return "No data"; // Return a default value in case of error
15 })
16 .finally(() => {
17 console.log("DNA analysis procedure complete — regardless of outcome.");
18 // finally takes no arguments and returns nothing
19 });
20
21console.log("Continuing work while DNA is being analyzed...");Result (on success):
1Sending sample to the laboratory...
2Analyzing dinosaur DNA sample...
3Continuing work while DNA is being analyzed...
4(after 3 seconds)
5DNA analysis completed successfully!
6Species: Velociraptor
7Age: Approximately 68 million years
8Diet: Carnivore
9Genetic markers: ACGT-237, TGGC-118, GACT-339
10DNA analysis procedure complete — regardless of outcome.One of the greatest advantages of Promises is the ability to chain asynchronous operations. Each call to
.then() returns a new Promise, allowing the creation of sequences of asynchronous operations without nesting.Let's imagine the process of creating a new dinosaur in Jurassic Park:
1function fetchDNAFromDatabase(speciesId) {
2 return new Promise((resolve, reject) => {
3 console.log(`Fetching DNA for species ID: ${speciesId}...`);
4 setTimeout(() => {
5 if (speciesId > 0) {
6 resolve(`dna_sequence_${speciesId}`);
7 } else {
8 reject(new Error("Invalid species ID"));
9 }
10 }, 1000);
11 });
12}
13
14function prepareEmbryo(dnaSequence) {
15 return new Promise((resolve, reject) => {
16 console.log(`Preparing embryo from DNA sequence: ${dnaSequence}...`);
17 setTimeout(() => {
18 if (dnaSequence.includes("dna_sequence")) {
19 resolve(`embryo_${dnaSequence}`);
20 } else {
21 reject(new Error("Invalid DNA sequence"));
22 }
23 }, 1500);
24 });
25}
26
27function incubateEgg(embryo) {
28 return new Promise((resolve, reject) => {
29 console.log(`Incubating egg with embryo: ${embryo}...`);
30 setTimeout(() => {
31 if (embryo.includes("embryo_")) {
32 resolve(`dino_baby_${embryo.replace("embryo_", "")}`);
33 } else {
34 reject(new Error("Invalid embryo"));
35 }
36 }, 2000);
37 });
38}
39
40function raiseDinosaur(baby) {
41 return new Promise((resolve, reject) => {
42 console.log(`Raising young dinosaur: ${baby}...`);
43 setTimeout(() => {
44 if (baby.includes("dino_baby_")) {
45 resolve(`adult_dino_${baby.replace("dino_baby_", "")}`);
46 } else {
47 reject(new Error("Invalid young dinosaur"));
48 }
49 }, 1500);
50 });
51}
52
53// Using a promise chain
54console.log("Starting the dinosaur creation process...");
55
56fetchDNAFromDatabase(5)
57 .then(dna => {
58 console.log(`DNA fetched: ${dna}`);
59 return prepareEmbryo(dna); // Return a new Promise
60 })
61 .then(embryo => {
62 console.log(`Embryo prepared: ${embryo}`);
63 return incubateEgg(embryo); // Return a new Promise
64 })
65 .then(baby => {
66 console.log(`Dinosaur hatched: ${baby}`);
67 return raiseDinosaur(baby); // Return a new Promise
68 })
69 .then(adultDino => {
70 console.log(`Dinosaur ready to be introduced to the park: ${adultDino}`);
71 })
72 .catch(error => {
73 console.error("Error in dinosaur creation process:", error.message);
74 })
75 .finally(() => {
76 console.log("Dinosaur creation process complete.");
77 });Compare that with the callback version, which would look something like this:
1fetchDNAFromDatabase(5, function(error, dna) {
2 if (error) {
3 console.error("Error fetching DNA:", error.message);
4 console.log("Dinosaur creation process complete.");
5 return;
6 }
7
8 console.log(`DNA fetched: ${dna}`);
9 prepareEmbryo(dna, function(error, embryo) {
10 if (error) {
11 console.error("Error preparing embryo:", error.message);
12 console.log("Dinosaur creation process complete.");
13 return;
14 }
15
16 console.log(`Embryo prepared: ${embryo}`);
17 incubateEgg(embryo, function(error, baby) {
18 if (error) {
19 console.error("Error incubating egg:", error.message);
20 console.log("Dinosaur creation process complete.");
21 return;
22 }
23
24 console.log(`Dinosaur hatched: ${baby}`);
25 raiseDinosaur(baby, function(error, adultDino) {
26 if (error) {
27 console.error("Error raising dinosaur:", error.message);
28 console.log("Dinosaur creation process complete.");
29 return;
30 }
31
32 console.log(`Dinosaur ready to be introduced to the park: ${adultDino}`);
33 console.log("Dinosaur creation process complete.");
34 });
35 });
36 });
37});As you can see, the Promise version is much more readable and easier to maintain.
We often need to work with APIs or libraries that use callback-based functions. We can convert them to Promises using a wrapper function:
1// Callback-based function (Node-style)
2function fetchEnclosureData(enclosureId, callback) {
3 setTimeout(() => {
4 if (enclosureId <= 0) {
5 callback(new Error("Invalid enclosure ID"));
6 return;
7 }
8
9 const enclosureData = {
10 id: enclosureId,
11 name: `Enclosure ${enclosureId}`,
12 area: 1000 * enclosureId,
13 type: enclosureId % 2 === 0 ? "Terrestrial" : "Aquatic"
14 };
15
16 callback(null, enclosureData);
17 }, 1000);
18}
19
20// Convert to Promise
21function fetchEnclosureDataPromise(enclosureId) {
22 return new Promise((resolve, reject) => {
23 fetchEnclosureData(enclosureId, (error, data) => {
24 if (error) {
25 reject(error); // Reject the Promise on error
26 } else {
27 resolve(data); // Resolve the Promise with data
28 }
29 });
30 });
31}
32
33// Using the Promise version
34fetchEnclosureDataPromise(3)
35 .then(data => {
36 console.log("Enclosure data:", data);
37 })
38 .catch(error => {
39 console.error("Error:", error.message);
40 });This pattern is so common that Node.js offers a built-in
util.promisify() function to automatically convert callback-based functions to functions that return Promises.JavaScript provides several static methods on the Promise object that are very useful in various scenarios:
These methods create a Promise that is immediately resolved or rejected with a given value:
1// Immediate resolution
2const resolvedPromise = Promise.resolve("Dinosaur data");
3
4resolvedPromise.then(data => {
5 console.log("Resolved with:", data); // "Resolved with: Dinosaur data"
6});
7
8// Immediate rejection
9const rejectedPromise = Promise.reject(new Error("No access to dinosaur"));
10
11rejectedPromise.catch(error => {
12 console.error("Rejected with:", error.message); // "Rejected with: No access to dinosaur"
13});Takes an array of promises and returns a new promise that resolves when all promises in the array resolve, or rejects when any one of them rejects:
1// Fetching data about three dinosaurs in parallel
2const dino1Promise = fetchDinosaurData(1);
3const dino2Promise = fetchDinosaurData(2);
4const dino3Promise = fetchDinosaurData(3);
5
6Promise.all([dino1Promise, dino2Promise, dino3Promise])
7 .then(results => {
8 // results is an array of results in promise order
9 console.log("All dinosaur data fetched:");
10 console.log("Dinosaur 1:", results[0]);
11 console.log("Dinosaur 2:", results[1]);
12 console.log("Dinosaur 3:", results[2]);
13
14 // We can now do something with all data at once
15 const totalWeight = results.reduce((sum, dino) => sum + dino.weight, 0);
16 console.log(`Total weight of all dinosaurs: ${totalWeight} kg`);
17 })
18 .catch(error => {
19 // If even one promise rejects, the entire operation fails
20 console.error("Failed to fetch all dinosaur data:", error.message);
21 });Takes an array of promises and returns a new promise that resolves or rejects with the result of the first promise to settle:
1// Simulating monitoring of different park systems
2function monitorFence() {
3 return new Promise((resolve, reject) => {
4 setTimeout(() => {
5 if (Math.random() > 0.8) {
6 reject(new Error("Fence failure in sector B!"));
7 } else {
8 resolve("Fence operating normally");
9 }
10 }, 1000 + Math.random() * 4000); // 1-5 seconds
11 });
12}
13
14function monitorPower() {
15 return new Promise((resolve, reject) => {
16 setTimeout(() => {
17 if (Math.random() > 0.9) {
18 reject(new Error("Main power failure!"));
19 } else {
20 resolve("Power operating normally");
21 }
22 }, 1000 + Math.random() * 4000); // 1-5 seconds
23 });
24}
25
26function monitorCameras() {
27 return new Promise((resolve, reject) => {
28 setTimeout(() => {
29 if (Math.random() > 0.7) {
30 reject(new Error("Camera signal lost in sector D!"));
31 } else {
32 resolve("Cameras operating normally");
33 }
34 }, 1000 + Math.random() * 4000); // 1-5 seconds
35 });
36}
37
38// We want to know the first alert that appears
39Promise.race([
40 monitorFence(),
41 monitorPower(),
42 monitorCameras()
43])
44 .then(status => {
45 console.log("First system status:", status);
46 })
47 .catch(error => {
48 console.error("ALARM! First detected problem:", error.message);
49 // Trigger emergency procedures here
50 triggerEmergencyProcedure();
51 });
52
53function triggerEmergencyProcedure() {
54 console.log("Triggering emergency procedure...");
55 // Emergency procedure code
56}Introduced in ES2020, takes an array of promises and returns a new promise that resolves when all promises in the array complete (either resolved or rejected):
1// Monitoring all park systems
2Promise.allSettled([
3 monitorFence(),
4 monitorPower(),
5 monitorCameras()
6])
7 .then(results => {
8 console.log("Status of all systems:");
9
10 // Each result has status 'fulfilled' or 'rejected'
11 results.forEach((result, index) => {
12 const systemNames = ["Fence", "Power", "Cameras"];
13
14 if (result.status === 'fulfilled') {
15 console.log(`${systemNames[index]}: OK - ${result.value}`);
16 } else {
17 console.log(`${systemNames[index]}: ERROR - ${result.reason.message}`);
18 }
19 });
20
21 // Check if any system raised an alarm
22 const failures = results.filter(result => result.status === 'rejected');
23 if (failures.length > 0) {
24 console.log(`WARNING: ${failures.length} system failures detected!`);
25 triggerEmergencyProcedure();
26 }
27 });Proper error handling is critical, especially in asynchronous code. Promises provide a unified way to handle errors using the
catch() method:1fetchDNAFromDatabase(-1) // Invalid ID, will cause an error
2 .then(dna => {
3 console.log(`DNA fetched: ${dna}`);
4 return prepareEmbryo(dna);
5 })
6 .then(embryo => {
7 console.log(`Embryo prepared: ${embryo}`);
8 return incubateEgg(embryo);
9 })
10 .then(baby => {
11 console.log(`Dinosaur hatched: ${baby}`);
12 return raiseDinosaur(baby);
13 })
14 .then(adultDino => {
15 console.log(`Dinosaur ready to be introduced to the park: ${adultDino}`);
16 })
17 .catch(error => {
18 // This catch handles errors from all preceding Promises
19 console.error("Error in dinosaur creation process:", error.message);
20
21 // We can log errors, notify the user, or take corrective action here
22 notifyLabForResearch();
23
24 // We can also return a value to be passed to the next .then()
25 return "Dinosaur creation attempt failed";
26 })
27 .then(message => {
28 // This .then() will execute after .catch()
29 console.log(message); // "Dinosaur creation attempt failed"
30
31 // We can initiate cleanup procedures or take alternative actions here
32 return restartProcedure();
33 })
34 .catch(error => {
35 // This catch handles errors from restartProcedure()
36 console.error("Restart error:", error.message);
37 });
38
39function notifyLabForResearch() {
40 console.log("Notifying laboratory about DNA problem...");
41}
42
43function restartProcedure() {
44 console.log("Restarting procedure with backup DNA...");
45 return "Procedure restarted";
46}Important rules for error handling in Promises:
catch() method catches errors from all preceding then() methods.then() function, the Promise will be rejected with that exceptioncatch() methods in a Promise chain to handle different types of errorscatch() you can continue the chain with more then() methodsA common mistake is forgetting to return a Promise from a function, leading to unexpected behavior:
1// Wrong — not returning the Promise from the function
2function fetchAndSaveData(id) {
3 fetchDinosaurData(id) // This returns a Promise, but we do not propagate it
4 .then(data => {
5 return saveData(data); // This Promise is not visible outside
6 });
7
8 // The function does not explicitly return a value, so it returns undefined by default
9}
10
11// Correct — returning the Promise
12function fetchAndSaveData(id) {
13 return fetchDinosaurData(id) // Returning the Promise
14 .then(data => {
15 return saveData(data); // This Promise is now part of the chain
16 });
17}Unhandled rejected Promises can cause hard-to-diagnose problems:
1// Bad — no error handling
2fetchDinosaurData(999) // ID does not exist
3 .then(data => {
4 // This code will not execute when the Promise is rejected
5 displayData(data);
6 });
7// If the Promise is rejected, the error is not handled!
8
9// Good — always handle errors
10fetchDinosaurData(999)
11 .then(data => {
12 displayData(data);
13 })
14 .catch(error => {
15 console.error("Failed to fetch data:", error.message);
16 displayError(error);
17 });Although Promises help avoid Callback Hell, you can fall into a similar trap by nesting Promises instead of chaining them:
1// Bad — nesting Promises (Promise Hell)
2fetchDinosaurData(1)
3 .then(dino => {
4 fetchEnclosureData(dino.enclosureId) // Nested Promise
5 .then(enclosure => {
6 fetchStaff(enclosure.id) // Another nested Promise
7 .then(staff => {
8 console.log("Dinosaur data:", dino);
9 console.log("Enclosure data:", enclosure);
10 console.log("Staff:", staff);
11 });
12 });
13 });
14
15// Good — chaining Promises
16fetchDinosaurData(1)
17 .then(dino => {
18 dinoData = dino; // Save data for later use
19 return fetchEnclosureData(dino.enclosureId); // Return Promise
20 })
21 .then(enclosure => {
22 enclosureData = enclosure; // Save data for later use
23 return fetchStaff(enclosure.id); // Return Promise
24 })
25 .then(staff => {
26 console.log("Dinosaur data:", dinoData);
27 console.log("Enclosure data:", enclosureData);
28 console.log("Staff:", staff);
29 })
30 .catch(error => {
31 console.error("An error occurred:", error.message);
32 });Promises are a powerful JavaScript mechanism for handling asynchronous operations, offering many advantages over traditional callbacks:
.catch()Promise.all() and Promise.race()Promises form the foundation for an even newer async mechanism in JavaScript —
async/await, which we will cover in future lessons. In Jurassic Park, as in real programming, good management of asynchronous operations is the key to building reliable and maintainable systems.In the next exercise we will see how to chain Promises and build more complex asynchronous flows.