article

Promises in JavaScript: Trust Issues in Code

3 min read

Promises in JavaScript are like the friend who says they’ll help you move and shows up three hours late. In this article, we unpack promises, async/await, and how to stop chaining .then() like it’s a bad habit.

What Is a Promise?

A Promise is an object representing the eventual completion (or failure) of an asynchronous operation. It is always in one of three states:

const promise = new Promise((resolve, reject) => {
  setTimeout(() => resolve("done"), 1000);
});

promise.then((result) => console.log(result)); // "done" after 1 s

The .then() Chain

You can chain .then() calls to sequence async work. Each .then() receives the return value of the previous one.

fetch("/api/user")
  .then((res) => res.json())
  .then((user) => fetch(`/api/posts?userId=${user.id}`))
  .then((res) => res.json())
  .then((posts) => console.log(posts))
  .catch((err) => console.error(err));

It works, but deeply nested chains get messy fast — hence async/await.

Enter async/await

async/await is syntactic sugar over promises. An async function always returns a promise; await pauses execution inside it until a promise settles.

async function loadUserPosts(userId) {
  const res  = await fetch(`/api/user/${userId}`);
  const user = await res.json();

  const postsRes = await fetch(`/api/posts?userId=${user.id}`);
  return postsRes.json();
}

Reads like synchronous code. Runs asynchronously.

Error Handling

Wrap await calls in a try/catch block — it catches both synchronous throws and rejected promises.

async function loadData() {
  try {
    const res  = await fetch("/api/data");
    const data = await res.json();
    return data;
  } catch (err) {
    console.error("Something went wrong:", err);
  }
}

Avoid the common mistake of forgetting catch — unhandled rejections will surface as runtime warnings or errors depending on your environment.

Running Promises in Parallel

await inside a loop runs sequentially. If the tasks are independent, use Promise.all instead.

// Sequential — slow
for (const id of ids) {
  await fetchPost(id);
}

// Parallel — fast
const posts = await Promise.all(ids.map((id) => fetchPost(id)));

Promise.allSettled is useful when you want all results regardless of whether some fail.

Common Pitfalls

Forgetting to await — the function returns a pending promise, not the resolved value.

async function bad() {
  const data = fetch("/api"); // missing await — data is a Promise, not a Response
}

Mixing .then() and await in the same flow — pick one style per function and stick to it.

Takeaway

Promises are the foundation of async JavaScript. async/await makes them readable. Use Promise.all when you can run things in parallel, always handle rejections, and remember: the function never actually waits — it just looks like it does.