Stop Struggling With JavaScript Promises: Here’s a Simple Explanation

What is a Promise?

Consider a Promise as a temporary substitute for a value JavaScript will obtain later. You’re basically saying to JavaScript: “Go do this thing. I’ll wait. Just let me know when you’re done“.

Every Promise is in one of three states:

  • Pending: It hasn’t finished yet.
  • Fulfilled: It worked! You got your result.
  • Rejected: Something went wrong.

Then, Catch, and Finally

Promises come with methods you can chain together.

JavaScript’s try…catch…finally structure manages errors without stopping execution.
(click on the image to open in a new tab)

Line-by-Line Explanation

fetch('https://api.example.com/data')

This line initiates an HTTP request. It tells the browser:

“Go to this URL and try to fetch data from it.”

  • If the server responds (even with an error status), fetch() returns a Promise that resolves to a Response object.
  • If there’s a network error (like no internet or a blocked domain), the Promise is rejected and jumps straight to .catch().

.then(response => response.json())

The .then() function executes once the response is received.

  • The response object holds details like headers, status codes, and the body.
  • response.json() is a method that reads the response body and parses it as JSON.
  • This also returns a Promise—because reading and parsing the stream is done asynchronously.

Why is reading and parsing the response stream done asynchronously?

When you make a fetch() request, you don’t get the entire response body of the request all at once.

Instead, it comes as a stream, a sequence of small pieces that arrive over time.

Think of it like downloading a movie: you don’t get the entire file all at once. It arrives in parts; your browser stores these parts so you can begin using it while it’s still downloading.

The same idea applies to JSON, HTML, images, etc.

So parsing the response body — even if it’s just JSON — involves:

  1. Waiting for all data chunks to arrive
  2. Stitching those chunks together into a full string
  3. Parsing the string into a JavaScript object (via JSON.parse)

This is why it returns a Promise that eventually resolves with the final parsed object.

.then(data => console.log(data))

Now, once the JSON is successfully parsed, we get to the actual result: the data.

  • This is usually an object or array, like:
    { name: “ChatGPT”, version: “4.5” }
  • console.log(data) prints it out in the browser’s console so we can see what came back.
Note: You could replace this with any logic you want.

.catch(error => console.error(error))

If anything goes wrong—whether it’s a network failure, invalid JSON, or a thrown error—this catch() will handle it.

  • error is the reason the Promise was rejected.
  • You typically log it or show a user-friendly message.
Note: This is your safety net. Without it, your app might silently fail or throw a confusing error in the console.

.finally(() => console.log('Done'))

.finally() is a method you can chain to a Promise. It’s used to define code that should run after the Promise is completed, no matter what the outcome — fulfilled or rejected.

This is especially useful when you’re doing things like:

  • Hiding a loading spinner
  • Re-enabling a form button
  • Closing a modal

You want those things to happen regardless of whether the operation succeeded or failed.

Why Not Just Wait?

JavaScript can only do one thing at a time. If it were to wait, it would freeze everything else. Interaction with buttons, animations, typing or scrolling and more, would have to wait. That’s a critical flaw in web development.

In modern, interactive applications, responsiveness is everything. Promises allow JavaScript to be non-blocking, thus your app can stay fast, smooth, and user-friendly.

And that’s critical on the web where speed and responsiveness can make or break the user experience.

A Real-World Analogy

Let’s say you’re cooking dinner.

You put water on to boil and then—while you’re waiting—you chop veggetables, set the table, and maybe even wash some dishes. You’re doing other things while the water is heating up.

But imagine if you just stood there, staring at the pot, refusing to move until it boiled.

That’s what blocking code would do. It halts the entire program just to wait for one task. It’s unproductive and makes for a bad user experience, like an app that freezes just because it’s waiting for a server response.

A Blocking Example (the wrong way):

This program waits for getDataFromServer() to finish execution which is a problem if the server is slow.
(click on the image to open in a new tab)

In this code who following a procedure. First, we get what getDataFromServer() returns.

And then, we call the processData() function passing the returned value as an argument.

If the server is slow, the whole program stops until getDataFromServer() finishes.

With Promises we can avoid this.

How JavaScript Handles It the Right Way

The previous code utilizing Promises.
(click on the image to open in a new tab)

In this case:

  • JavaScript sends the request.
  • Keeps going! (It doesn’t wait—console.log() runs immediately.)
  • Once the data returns, it processes it.

Meanwhile, your UI stays responsive.

Conclusion

JavaScript Promises effectively handle the common issue of delayed tasks.

Once you’ve mastered them, your code becomes cleaner, more responsive, and a lot easier to manage when things get unpredictable.

Although my blog doesn’t support comments, feel free to reply via email or X.