DEV Community

Cover image for JavaScript Promises: Transform Your Async Code from Messy to Clean
Kathirvel S
Kathirvel S

Posted on

JavaScript Promises: Transform Your Async Code from Messy to Clean

It’s late at night.

You open Instagram just for a quick scroll.
The app loads… but something feels off.

The profile takes time.
The feed shows up half-loaded.
Likes appear late.
Comments suddenly pop in after a delay.

For a moment, it feels broken.

Now imagine you’re the developer behind that experience.

Somewhere in your code:

  • Data is coming late
  • Steps are not properly ordered
  • One thing depends on another… but the flow isn’t clear

This is exactly where most developers struggle — not with writing code, but with controlling when things happen.

Let’s make this simple


Step 1: The “Do Everything in Order” Phase (Synchronous JavaScript)

In the beginning, everything feels easy.

Your code runs top to bottom. No surprises.

console.log("1. User");
console.log("2. Feed");
console.log("3. Likes");
Enter fullscreen mode Exit fullscreen mode

Output:

1. User
2. Feed
3. Likes
Enter fullscreen mode Exit fullscreen mode

Nice and predictable.

But imagine this:

What if loading the user takes 3 seconds?

Everything below just waits.

Your app is now just… staring at the user.


Step 2: “Let’s Not Make Users Wait” (Asynchronous JavaScript)

So you try something smarter.

console.log("Start");

setTimeout(() => {
  console.log("User loaded");
}, 2000);

console.log("Keep using app...");
Enter fullscreen mode Exit fullscreen mode

Output:

Start
Keep using app...
User loaded
Enter fullscreen mode Exit fullscreen mode

Now your app feels alive.

Things are loading in the background.

But there’s a small problem:

Things don’t happen in order anymore.


Step 3: “Okay, I Want Control” (Callbacks)

You decide:

“I want things in order… but still async.”

function loadUser(next) {
  setTimeout(() => {
    console.log("User");
    next();
  }, 1000);
}

function loadFeed(next) {
  setTimeout(() => {
    console.log("Feed");
    next();
  }, 1000);
}

loadUser(() => {
  loadFeed(() => {
    console.log("Done");
  });
});
Enter fullscreen mode Exit fullscreen mode

Output:

User
Feed
Done
Enter fullscreen mode Exit fullscreen mode

Works well.

But if you keep adding steps…


Step 4: The “Why Is This So Deep?” Moment (Callback Hell)

loadUser(() => {
  loadFeed(() => {
    setTimeout(() => {
      console.log("Likes");
      setTimeout(() => {
        console.log("Comments");
        console.log("All done");
      }, 1000);
    }, 1000);
  });
});
Enter fullscreen mode Exit fullscreen mode

It works.

But reading it feels like going down a staircase.

Not fun. Easy to mess up.


Step 5: A Cleaner Way (Promises)

Now let’s clean things up.

function loadUser() {
  return Promise.resolve("User");
}

function loadFeed() {
  return Promise.resolve("Feed");
}
Enter fullscreen mode Exit fullscreen mode

Now write flow like this:

loadUser()
  .then((res) => {
    console.log(res);
    return loadFeed();
  })
  .then((res) => {
    console.log(res);
    console.log("Done");
  });
Enter fullscreen mode Exit fullscreen mode

Output:

User
Feed
Done
Enter fullscreen mode Exit fullscreen mode

Much better.

Flat. Easy to follow.


Going Deeper: What a Promise Really Is

A Promise is not just a cleaner syntax.

It’s an object that represents a value that will be available later.

It has three states:

  • Pending → still waiting
  • Fulfilled → success (you get data)
  • Rejected → failed (you get error)

Here’s a simple example:

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("Data loaded");
    // reject("Something went wrong");
  }, 1000);
});

promise
  .then((data) => console.log(data))
  .catch((err) => console.log(err));
Enter fullscreen mode Exit fullscreen mode

Why this matters:

  • resolve() moves the promise to success
  • reject() moves it to failure
  • .then() handles success
  • .catch() handles errors

Small but Powerful Features of Promises

1. Chaining keeps things clean

Each .then() passes data to the next:

Promise.resolve(1)
  .then((num) => num + 1)
  .then((num) => num + 1)
  .then((num) => console.log(num)); // 3
Enter fullscreen mode Exit fullscreen mode

No nesting. Just flow.


2. Handling multiple async tasks

Sometimes tasks don’t depend on each other.

You can run them together:

Promise.all([
  Promise.resolve("User"),
  Promise.resolve("Feed"),
  Promise.resolve("Likes"),
]).then((res) => console.log(res));
Enter fullscreen mode Exit fullscreen mode

Output:

["User", "Feed", "Likes"]
Enter fullscreen mode Exit fullscreen mode

Everything runs in parallel.

Faster and efficient.


3. First result wins

Promise.race([
  new Promise((res) => setTimeout(() => res("Fast"), 1000)),
  new Promise((res) => setTimeout(() => res("Slow"), 3000)),
]).then(console.log);
Enter fullscreen mode Exit fullscreen mode

Output:

Fast
Enter fullscreen mode Exit fullscreen mode

Useful for timeouts or fastest responses.


Step 6: The Easiest Way (Async/Await)

Now the cleanest version:

async function run() {
  const user = await loadUser();
  console.log(user);

  const feed = await loadFeed();
  console.log(feed);

  console.log("Done");
}

run();
Enter fullscreen mode Exit fullscreen mode

Same result.

But this feels natural.

Like reading normal code.


Where Promises Are Really Used (And Why They Matter)

This is not just theory.

Promises are used everywhere in real applications.

Think about apps like Instagram, YouTube, or Netflix.

When you open them:

  • Your profile is fetched
  • Your feed is loaded
  • Images and videos load separately
  • Likes and comments arrive later

All of these happen asynchronously.

Some steps depend on others:

  • You need user data before showing a personalized feed
  • You need posts before loading comments
  • You need authentication before API calls

Promises help manage all this without chaos.

They let your app stay fast, responsive, and organized at the same time.


The Real Upgrade: From “It Works” to “It Feels Smooth”

Here’s the shift that matters.

At first, you write code just to make things work.

Then things get a little complex.

Suddenly:

  • Order breaks
  • Code gets nested
  • Bugs become harder to trace

That’s when you realize:

This isn’t just about code.
It’s about flow.

Every step in your app is a small event.

If those events are not clearly controlled:

  • Data shows up late
  • UI feels inconsistent
  • Users feel something is off

But when you structure it right:

  • Things load in the right order
  • Background work doesn’t block the app
  • Code stays readable even as it grows

That’s the real role of Promises and async/await.

They don’t just fix syntax.

They fix how your app behaves.

And when the behavior is right, the user never notices the complexity behind it.

It just feels smooth.

Top comments (0)