We use cookies to enhance your experience on the site
CodeWorlds

Chaining Promises (then, catch, finally)

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.

Recap of Promise Methods

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 states
  • catch(onRejected)
    — registers a handler only for the rejected state (shorthand for
    .then(null, onRejected)
    )
  • finally(onFinally)
    — registers a handler that runs regardless of outcome

Each of these methods returns a new Promise, which is the foundation for chaining.

How Chaining Works

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  });

Sequential Operations — Dinosaur Lab Pipeline

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  });

Return Values from then() and catch()

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  });

Error Handling in the Promise Chain

There are several strategies for handling errors in Promise chains:

1. Single catch at the end of the chain

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  });

2. Multiple catches for different stages

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}

3. Error handling inside the then() function

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}

Error Transformation

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  });

Nested vs Chained — A Key Distinction

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  });

Conditional Chaining

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");

Using finally for Cleanup

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);

Complex Operations with Promises

Sequential Execution of Asynchronous Operations on an Array

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  });

Executing Asynchronous Operations with a Limited Number of Parallel Tasks

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  });

Best Practices for Chaining Promises

  1. 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})
  2. 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  });
  3. 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  });
  4. 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}

Summary

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:

  1. Always return a Promise from functions in
    .then()
    to continue the chain
  2. Use
    .catch()
    for unified error handling
  3. Use
    .finally()
    for cleanup operations
  4. Avoid nesting Promises in favor of chaining
  5. Break complex operations into smaller, named functions

In the next exercise we will explore more advanced methods for working with multiple Promises simultaneously, such as

Promise.all()
,
Promise.race()
, and
Promise.allSettled()
.

Go to CodeWorlds