In the previous exercise we learned the basics of creating and using Promise objects. Now let's dive deeper into techniques for chaining promises to build more complex asynchronous flows. In a system as complex as Jurassic Park, we often need to execute sequences of asynchronous operations, handle errors, and ensure systems work correctly.
Before moving on to advanced techniques, let's recall the basic methods of the Promise object:
then(onFulfilled, onRejected) — registers handlers for fulfilled and rejected statescatch(onRejected) — registers a handler only for the rejected state (shorthand for .then(null, onRejected))finally(onFinally) — registers a handler that runs regardless of outcomeEach of these methods returns a new Promise, which is the foundation for chaining.
When you call
.then() on a Promise, the return value of the callback determines the state of the new Promise:1Promise.resolve("start")
2 .then(value => {
3 console.log(value); // "start"
4 return "second step"; // plain value → next Promise resolves with it
5 })
6 .then(value => {
7 console.log(value); // "second step"
8 return Promise.resolve("third step"); // Promise → next Promise resolves with its value
9 })
10 .then(value => {
11 console.log(value); // "third step"
12 throw new Error("something went wrong"); // throw → next Promise rejects
13 })
14 .catch(error => {
15 console.error(error.message); // "something went wrong"
16 return "recovered"; // return in catch → chain continues
17 })
18 .then(value => {
19 console.log(value); // "recovered"
20 });Let's build a full dinosaur creation pipeline in our Jurassic Park lab:
1// Step functions — each returns a Promise
2function collectDNASample(dinosaurId) {
3 return new Promise((resolve, reject) => {
4 setTimeout(() => {
5 if (dinosaurId > 0) {
6 resolve({ id: dinosaurId, dna: `ACGT-${dinosaurId}-TGCA`, quality: "high" });
7 } else {
8 reject(new Error("Invalid dinosaur ID for DNA collection"));
9 }
10 }, 800);
11 });
12}
13
14function sequenceDNA(sample) {
15 return new Promise((resolve, reject) => {
16 setTimeout(() => {
17 if (sample.quality === "high") {
18 resolve({ ...sample, sequence: `SEQ-${sample.dna}-END`, confidence: 0.98 });
19 } else {
20 reject(new Error(`DNA quality too low: ${sample.quality}`));
21 }
22 }, 1200);
23 });
24}
25
26function synthesizeProtein(sequencedDNA) {
27 return new Promise(resolve => {
28 setTimeout(() => {
29 resolve({
30 ...sequencedDNA,
31 protein: `PROTEIN-${sequencedDNA.id}`,
32 aminoAcids: 847
33 });
34 }, 1000);
35 });
36}
37
38function createEmbryo(proteinData) {
39 return new Promise((resolve, reject) => {
40 setTimeout(() => {
41 const success = Math.random() > 0.2;
42 if (success) {
43 resolve({ embryoId: `EMB-${proteinData.id}`, viability: "high", source: proteinData });
44 } else {
45 reject(new Error("Embryo creation failed — insufficient protein"));
46 }
47 }, 1500);
48 });
49}
50
51// Full pipeline
52console.log("=== Jurassic Park DNA Lab — Starting Pipeline ===");
53
54collectDNASample(7)
55 .then(sample => {
56 console.log(`Step 1 — DNA collected: ${sample.dna} (quality: ${sample.quality})`);
57 return sequenceDNA(sample);
58 })
59 .then(sequenced => {
60 console.log(`Step 2 — DNA sequenced: ${sequenced.sequence} (confidence: ${sequenced.confidence})`);
61 return synthesizeProtein(sequenced);
62 })
63 .then(protein => {
64 console.log(`Step 3 — Protein synthesized: ${protein.protein} (${protein.aminoAcids} amino acids)`);
65 return createEmbryo(protein);
66 })
67 .then(embryo => {
68 console.log(`Step 4 — Embryo created: ${embryo.embryoId} (viability: ${embryo.viability})`);
69 console.log("Pipeline complete! Embryo ready for incubation.");
70 return embryo;
71 })
72 .catch(error => {
73 console.error(`Pipeline failed: ${error.message}`);
74 console.log("Initiating cleanup procedures...");
75 })
76 .finally(() => {
77 console.log("=== Lab Pipeline Finished ===");
78 });The value returned from a callback in
.then() or .catch() becomes the value that the Promise returned by that method resolves with. This gives us the ability to transform data at every stage of the chain.1// Example of data transformation in a Promise chain
2fetchDinosaurInfo(5)
3 .then(dinoData => {
4 // Transform dinosaur data into the format used by the UI
5 return {
6 id: dinoData.id,
7 name: dinoData.name || "Unknown",
8 display: `${dinoData.species} (${dinoData.age} lat)`,
9 dangerLevel: dinoData.carnivore ? "Wysoki" : "Niski",
10 location: dinoData.enclosureId
11 };
12 })
13 .then(uiData => {
14 // Add additional UI information
15 return {
16 ...uiData,
17 icon: uiData.dangerLevel === "High" ? "🔴" : "🟢",
18 description: `${uiData.icon} ${uiData.display} - Threat: ${uiData.dangerLevel}`
19 };
20 })
21 .then(finalData => {
22 console.log("Data for UI:", finalData);
23 updateDinoCard(finalData); // Update the user interface
24 });There are several strategies for handling errors in Promise chains:
1prepareTransport(7) // Dinosaur with ID 7 is too large for standard transport
2 .then(transportInfo => loadDinosaur(transportInfo))
3 .then(shipmentInfo => transportToPark(shipmentInfo))
4 .then(deliveryInfo => placeInEnclosure(deliveryInfo))
5 .then(enclosureInfo => {
6 console.log("Success:", enclosureInfo);
7 })
8 .catch(error => {
9 // This catch handles errors from all previous steps
10 console.error("An error occurred:", error.message);
11 notifyAdministration(error);
12 });1prepareTransport(7)
2 .then(transportInfo => loadDinosaur(transportInfo))
3 .catch(error => {
4 // This catch handles errors only from prepareTransport and loadDinosaur
5 console.error("Error during preparation or loading:", error.message);
6 return orderOversizedTransport(7); // Alternative path — return a new Promise
7 })
8 .then(info => {
9 // This then receives either the result of loadDinosaur or orderOversizedTransport
10 return transportToPark(info);
11 })
12 .then(deliveryInfo => placeInEnclosure(deliveryInfo))
13 .catch(error => {
14 // This catch handles errors from transportToPark, placeInEnclosure or orderOversizedTransport
15 console.error("Error during transport or enclosure placement:", error.message);
16 notifySecurityTeam(error);
17 throw error; // Pass the error further
18 })
19 .catch(error => {
20 // This catch catches all unhandled errors
21 console.error("Critical error in the process:", error.message);
22 activateEmergencyProcedures();
23 });
24
25function orderOversizedTransport(dinoId) {
26 console.log(`Ordering oversized transport for dinosaur #${dinoId}...`);
27 // Implementation returning a Promise
28 return Promise.resolve({
29 id: dinoId,
30 vehicle: "Reinforced special transport XL",
31 status: "Prepared"
32 });
33}1prepareTransport(3)
2 .then(transportInfo => {
3 try {
4 // Code that may throw exceptions
5 validateTransport(transportInfo);
6 return loadDinosaur(transportInfo);
7 } catch (error) {
8 // Handling synchronous exceptions
9 console.error("Transport validation error:", error.message);
10 return repairTransport(transportInfo); // Return a new Promise
11 }
12 })
13 .then(shipmentInfo => transportToPark(shipmentInfo))
14 .catch(error => {
15 // Handling remaining errors
16 console.error("Process error:", error.message);
17 });
18
19function validateTransport(transportInfo) {
20 if (!transportInfo.vehicle.includes("cage")) {
21 throw new Error("Transport does not have proper safety measures!");
22 }
23}
24
25function repairTransport(transportInfo) {
26 console.log("Repairing transport issues...");
27 return Promise.resolve({
28 ...transportInfo,
29 vehicle: transportInfo.vehicle + " with a reinforced cage",
30 status: "Repaired"
31 });
32}The
.catch() method can not only handle an error but also transform it into a success by returning a value:1fetchDinosaurInfo(999) // ID doesn't exist
2 .catch(error => {
3 console.log("Nie znaleziono dinosaura:", error.message);
4 // Instead of propagating the error, return default data
5 return {
6 id: 999,
7 name: "Unknown",
8 species: "Unknown species",
9 age: "?",
10 carnivore: true,
11 enclosureId: "N/A"
12 };
13 })
14 .then(dinoData => {
15 // This then will execute with default data
16 console.log("Dinosaur data (default or actual):", dinoData);
17 updateUI(dinoData);
18 });1// NESTED (bad) — creates promise hell, error handling is messy
2fetchDinosaurData(1).then(dino => {
3 fetchEnclosureData(dino.enclosureId).then(enclosure => {
4 fetchVetRecord(dino.id).then(record => {
5 // All three variables accessible here, but code is deeply nested
6 console.log(dino.name, enclosure.name, record.lastCheckup);
7 }).catch(e => console.error("vet error", e));
8 }).catch(e => console.error("enclosure error", e));
9}).catch(e => console.error("dino error", e));
10
11// CHAINED (good) — flat structure, single error handler
12let dinoRef, enclosureRef;
13
14fetchDinosaurData(1)
15 .then(dino => {
16 dinoRef = dino;
17 return fetchEnclosureData(dino.enclosureId);
18 })
19 .then(enclosure => {
20 enclosureRef = enclosure;
21 return fetchVetRecord(dinoRef.id);
22 })
23 .then(record => {
24 console.log(dinoRef.name, enclosureRef.name, record.lastCheckup);
25 })
26 .catch(error => {
27 // Catches any error from any step
28 console.error("Failed:", error.message);
29 });Sometimes we want to conditionally skip steps:
1function processEmergencyReport(reportId, urgency) {
2 return fetchReport(reportId)
3 .then(report => {
4 console.log(`Report received: ${report.title}`);
5
6 // Conditionally add extra step based on urgency
7 if (urgency === "critical") {
8 return notifyDirector(report).then(() => report); // Return report after notification
9 }
10 return report; // Skip notification for non-critical
11 })
12 .then(report => {
13 return logReport(report); // Always log
14 })
15 .then(() => {
16 console.log("Report processing complete.");
17 });
18}
19
20// Helper stubs
21function fetchReport(id) { return Promise.resolve({ id, title: `Report #${id}`, data: {} }); }
22function notifyDirector(r) { return new Promise(res => setTimeout(() => { console.log("Director notified"); res(); }, 300)); }
23function logReport(r) { return new Promise(res => setTimeout(() => { console.log(`Logged: ${r.title}`); res(r); }, 200)); }
24
25processEmergencyReport(42, "critical");
26processEmergencyReport(43, "low");finally is perfect for cleanup logic that must run regardless of success or failure:1function runLabExperiment(experimentId) {
2 let labResources = null;
3
4 return allocateLabResources(experimentId)
5 .then(resources => {
6 labResources = resources;
7 console.log(`Resources allocated: ${resources.resourceId}`);
8 return runExperiment(resources);
9 })
10 .then(result => {
11 console.log(`Experiment success: ${result.outcome}`);
12 return result;
13 })
14 .catch(error => {
15 console.error(`Experiment failed: ${error.message}`);
16 throw error; // Re-throw so caller knows it failed
17 })
18 .finally(() => {
19 // Always release resources, even if experiment failed
20 if (labResources) {
21 releaseLabResources(labResources);
22 console.log("Lab resources released.");
23 }
24 });
25}
26
27function allocateLabResources(id) {
28 return Promise.resolve({ resourceId: `LAB-RES-${id}`, equipment: ["centrifuge", "sequencer"] });
29}
30function runExperiment(resources) {
31 return new Promise((resolve, reject) => {
32 setTimeout(() => {
33 Math.random() > 0.3
34 ? resolve({ outcome: "DNA extracted successfully" })
35 : reject(new Error("Experiment contamination detected"));
36 }, 1000);
37 });
38}
39function releaseLabResources(resources) {
40 console.log(`Releasing ${resources.resourceId}`);
41}
42
43runLabExperiment(101);Sometimes we need to perform an asynchronous operation on each element of an array, one after another:
1// Sequential dinosaur feeding
2const dinosaurIds = [1, 2, 3, 4, 5];
3
4// Function for feeding one dinosaur
5function feedDinosaur(id) {
6 return new Promise((resolve, reject) => {
7 console.log(`Rozpoczynam karmienie dinosaura #${id}...`);
8 setTimeout(() => {
9 if (id === 3) {
10 reject(new Error(`Dinosaur #${id} odmawia jedzenia!`));
11 return;
12 }
13 console.log(`Dinosaur #${id} nakarmiony!`);
14 resolve({id, status: 'nakarmiony', time: new Date().toISOString()});
15 }, 1000);
16 });
17}
18
19// Sequential feeding of all dinosaurs
20dinosaurIds.reduce((promise, dinoId) => {
21 return promise
22 .then(results => {
23 // results is an array of results from previous operations
24 return feedDinosaur(dinoId)
25 .then(result => {
26 // Add the result to the results array
27 return [...results, result];
28 })
29 .catch(error => {
30 console.error(`Error feeding dinosaur #${dinoId}:`, error.message);
31 // Add error information to the results
32 return [...results, {id: dinoId, status: 'error', error: error.message}];
33 });
34 });
35}, Promise.resolve([])) // Start with an empty results array
36 .then(allResults => {
37 console.log("All dinosaurs processed:");
38 console.log(allResults);
39
40 // Check if all dinosaurs have been fed
41 const allFed = allResults.every(result => result.status === 'fed');
42 if (allFed) {
43 console.log("All dinosaurs have been fed!");
44 } else {
45 console.log("Not all dinosaurs have been fed - check the report.");
46 }
47 });1// Function for executing async operations with limited parallelism
2async function executeWithLimit(array, promiseFunction, parallelLimit) {
3 const allResults = [];
4
5 // Execute tasks in batches
6 for (let i = 0; i < array.length; i += parallelLimit) {
7 const batch = array.slice(i, i + parallelLimit);
8
9 // Run Promise.all for a batch of tasks
10 const results = await Promise.all(
11 batch.map(element =>
12 promiseFunction(element)
13 .catch(error => {
14 console.error(`Error for element ${element}:`, error.message);
15 return { element, status: 'error', error: error.message };
16 })
17 )
18 );
19
20 allResults.push(...results);
21 console.log(`Completed batch ${i / parallelLimit + 1} (${results.length} operacji)`);
22 }
23
24 return allResults;
25}
26
27// Using the function for parallel but limited health checks on dinosaurs
28const allDinosaurIds = Array.from({ length: 20 }, (_, i) => i + 1);
29
30function checkDinosaurHealth(id) {
31 return new Promise((resolve, reject) => {
32 console.log(`Checking health of dinosaur #${id}...`);
33 setTimeout(() => {
34 // Simulate random health status
35 const health = Math.floor(Math.random() * 100);
36 if (health < 30) {
37 reject(new Error(`Dinosaur #${id} ma problemy zdrowotne! Poziom zdrowia: ${health}`));
38 return;
39 }
40 console.log(`Dinosaur #${id} jest zdrowy. Poziom zdrowia: ${health}`);
41 resolve({id, health, status: 'healthy'});
42 }, 500 + Math.random() * 1000); // Random execution time
43 });
44}
45
46// Check health of at most 5 dinosaurs at a time
47executeWithLimit(allDinosaurIds, checkDinosaurHealth, 5)
48 .then(results => {
49 console.log("Health check of all dinosaurs completed:");
50
51 const zdrowe = results.filter(w => w.status === 'zdrowy').length;
52 const chore = results.filter(w => w.status === 'error').length;
53
54 console.log(`Healthy: ${zdrowe}, Requiring attention: ${chore}`);
55
56 // Display dinosaurs that need attention
57 if (sick > 0) {
58 console.log("Dinosaurs requiring veterinary intervention:");
59 results
60 .filter(w => w.status === 'error')
61 .forEach(w => console.log(`- Dinosaur #${w.element}: ${w.error}`));
62 }
63 });Always return a Promise from then() functions — this ensures proper chaining and result propagation:
1.then(data => {
2 // Bad — doesn't return a Promise, the chain will be broken
3 processData(data);
4})
5
6.then(data => {
7 // Good — returns the processing result to the next then()
8 return processData(data);
9})Use a single catch() for a group of related operations — this makes the code more readable:
1fetchEnclosureData(5)
2 .then(data => updateEnclosureStatus(data))
3 .then(status => notifyStaff(status))
4 .catch(error => {
5 // One catch for the entire enclosure update operation
6 handleEnclosureError(error);
7 });
8
9// New, unrelated operation with its own catch
10fetchGuestList()
11 .then(visitors => updateVisitorCount(visitors))
12 .catch(error => {
13 // Separate catch for visitor-related operations
14 handleVisitorError(error);
15 });Use finally() for cleanup code — regardless of success or failure:
1openEnclosureGate(enclosureId)
2 .then(() => moveDinosaur(dinoId, enclosureId))
3 .catch(error => {
4 logError("Move error:", error);
5 throw error; // Propagate the error further
6 })
7 .finally(() => {
8 // Always close the gate, regardless of the outcome
9 closeEnclosureGate(enclosureId);
10 });Avoid deeply nested Promise chains — break them down into smaller, named functions:
1// Instead of one long chain:
2function moveDinosaurToNewEnclosure(dinoId, newEnclosureId) {
3 return fetchDinosaurInfo(dinoId)
4 .then(dino => {
5 // Check current enclosure
6 return fetchEnclosureData(dino.enclosureId)
7 .then(oldEnclosure => {
8 // Prepare new enclosure
9 return fetchEnclosureData(newEnclosureId)
10 .then(newEnclosure => {
11 // Check compatibility
12 // ... more nested thens...
13 });
14 });
15 });
16}
17
18// Better — break into smaller functions:
19function moveDinosaurToNewEnclosure(dinoId, newEnclosureId) {
20 return fetchDinosaurInfoZAktualnymWybiegiem(dinoId)
21 .then(data => checkEnclosureCompatibility(data, newEnclosureId))
22 .then(data => executeTransfer(data));
23}
24
25function fetchDinosaurInfoZAktualnymWybiegiem(dinoId) {
26 let dinoData;
27 return fetchDinosaurInfo(dinoId)
28 .then(dino => {
29 dinoData = dino;
30 return fetchEnclosureData(dino.enclosureId);
31 })
32 .then(oldEnclosure => {
33 return { dino: dinoData, oldEnclosure };
34 });
35}
36
37function checkEnclosureCompatibility(data, newEnclosureId) {
38 return fetchEnclosureData(newEnclosureId)
39 .then(newEnclosure => {
40 // Check compatibility
41 return { ...data, newEnclosure };
42 });
43}
44
45function executeTransfer(data) {
46 // Transfer implementation
47 return Promise.resolve({ status: "completed", ...data });
48}Chaining Promises with
.then(), .catch(), and .finally() gives you powerful tools for building complex asynchronous flows. A well-designed Promise chain can make even complicated asynchronous operations, like managing dinosaur transport in Jurassic Park, readable and easy to maintain.Key principles:
.then() to continue the chain.catch() for unified error handling.finally() for cleanup operationsIn the next exercise we will explore more advanced methods for working with multiple Promises simultaneously, such as
Promise.all(), Promise.race(), and Promise.allSettled().