Leveraging .then() in Modern JavaScript

Two similar‑looking mistakes have different effects in a .then() chain. If a handler starts a promise‑based operation without returning that promise, later steps are disconnected from that asynchronous work. If a .catch() handler neither throws nor returns a rejected promise, the chain continues in a fulfilled state. One breaks sequencing; the other handles the rejection.
Introducing Prototype.then
The .then() method is a fundamental part of the Promise prototype in JavaScript. It provides a way to handle the eventual completion (or failure) of asynchronous operations, whilst offering a cleaner, more manageable approach to asynchronous code compared to older techniques you may be familiar with like callbacks.
Syntax
promise.then(onFulfilled, onRejected)promise: The Promise object to which.then()is attached.onFulfilled: A function that is called when the Promise successfully resolves.onRejected: A function that is called when the Promise is rejected.
A Basic Example
const myPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Data fetched successfully');
}, 2000);
});
myPromise.then(
(data) => console.log(data), //=> onFulfilled
(error) => console.error(error) //=> onRejected
);Here, I'm approximating an asynchronous operation in myPromise by using setTimeout. Once the promise is resolved, .then() processes the result.
Real‑World Scenarios
The timer above gives us a small example to trace. In application code, we often receive a promise from an API such as fetch() instead. Let's look at how .then() handles that work.
API Calls
Any modern JavaScript application is likely to utilise API calls. I use them fairly extensively behind the scenes even here on my personal website, not least for things like the live weather descriptions, and the contact form. So, it is fair to say that .then() is particularly useful for handling API calls, allowing you to deal with data once it is available:
fetch('https://api.example.com/data')
.then((response) => {
if (!response.ok) {
throw new Error(`Request failed: HTTP ${response.status}`);
}
return response.json();
})
.then((data) => console.log(data))
.catch((error) => console.error('Error:', error));This is a terminal logging example: it checks response.ok, parses the JSON, and logs either the data or an error. fetch() does not reject just because the server returns an HTTP error status, so that status check matters. The final .catch() handles the rejection by logging it; if a caller needs to respond to the failure, return the chain and choose deliberately whether to rethrow or return a recovery value.
Chaining Promises
Another key strength of .then() is its ability to chain promises, making it possible to execute a sequence of asynchronous operations in a clear and concise manner one after the other. For example:
getData()
.then((data) => processData(data))
.then((processedData) => displayData(processedData))
.catch((error) => console.error(error));Each .then() waits for the previous operation to complete before executing, which ensures that the operations occur in the desired order, and when all proceeding data is available and in the required format.
Benefits of Using Prototype.then
Improved Readability
: Compared to nested callbacks,.then()offers a more readable and structured approach to handling asynchronous code.Error Handling
: It allows centralised and efficient error handling with.catch().Composability
: Promises can be composed and chained, enhancing the maintainability of the code.
then() vs. async/await
async and await are often easier to read when asynchronous code has several steps. That does not make .then() obsolete. Promise chains are still useful when you are returning a transformed promise directly, composing small operations or working in a style that benefits from expression‑like flow.
The important thing is consistency. Mixing .then() and await in the same small block can make error handling harder to follow unless there is a clear reason.
Return Values and Errors
.then() returns a new promise. Its handler can return a plain value for the next step, throw to reject the chain, or return another promise whose state the chain will adopt. A missing return can leave later asynchronous work outside the chain, while a .catch() handler that completes normally has handled the rejection and turns the chain back into fulfilment.
Wrapping Up
A .then() handler should return the value or promise that the next step depends on. In particular, a promise created inside the handler must be returned when the chain should adopt and wait for its state. A .catch() handler recovers unless it throws or returns a rejected promise, so recovery and propagation should be chosen explicitly.