Skip to main content

Command Palette

Search for a command to run...

JavaScript Promises Explained for Beginners

Deep dive into Promises of JS

Published
5 min read
JavaScript Promises Explained for Beginners
G
I enjoy blending technology with business to build solutions that create real impact. I’m fascinated by how technology empowers people, businesses, and startups. I believe thoughtful use of technology quietly improves lives, and that belief fuels everything I build and explore 💻.

Let’s start from the very first thing that is important here which is what problem promises are actually solving.

In JavaScript, we know that it is single threaded. That means it runs one thing at a time on a single thread. Now suppose we run some operation which is taking time, like fetching data from an API or reading a file. That process will not complete instantly, it will take some time.

Now if JavaScript was strictly blocking, then till that process completes, nothing else would run. Everything would just wait. That is obviously not practical because your entire application would feel stuck.

So to handle this, we have asynchronous processes. What happens here is that these time taking operations are moved to the background. JavaScript does not wait for them to finish immediately. Instead, it continues executing the rest of the code.

And once that background work is done, then its result comes back.

Now the question is how do we handle that result in a clean way. Earlier this was done using callbacks. But callbacks started creating problems when things became complex. You would have callbacks inside callbacks and that becomes very hard to read and manage.

This is where promises come into picture.

So what exactly is a promise.

As the name suggests, it is something that will give you a value in the future. Not immediately, but after some time.

In JavaScript, a promise is basically an object. It is created using the Promise class which JavaScript provides by default.

When we do new Promise, we are creating a new instance of that class, which means a new promise object.

Now this promise object internally has states. These are pending, fulfilled and rejected.

These are not something you can access directly like properties. You cannot do promise.state or something like that. These are internal states that JavaScript manages.

When a promise is created, it starts with pending. That means the work is still going on.

If the work completes successfully, it becomes fulfilled.

If something goes wrong, it becomes rejected.

Now one important thing to understand is how we actually create a promise.

Whenever we do new Promise, it expects a function inside it. This function is what we call a callback function.

And this function receives two arguments which are resolve and reject.

Both of these are functions.

So structure looks like this:

const promise = new Promise((resolve, reject) => {
  // some work
});

Now what we do inside this function is we perform some asynchronous work. If that work succeeds, we call resolve and pass a value. If that work fails, we call reject and pass an error. Until either resolve or reject is called, the promise stays in pending state.

Now coming to what resolve and reject actually do.

Resolve is used when the operation is successful. Whatever value you pass inside resolve becomes the final output of that promise. Reject is used when something fails. Whatever you pass inside reject becomes the error. So you can think of it like resolve gives success value and reject gives error value. Now once a promise is created, we need a way to handle these outcomes.

This is where then and catch come in. "then" is used to handle the success case. "catch" is used to handle the failure case.

Example:

promise
  .then(value => {
    console.log(value);
  })
  .catch(error => {
    console.log(error);
  });

So whenever resolve is called, then runs.

Whenever reject is called, catch runs.

Now if you are using async and await, then things look slightly different but internally it is the same concept. Await basically pauses the execution of that function until the promise settles. That means until it becomes either fulfilled or rejected. If it is fulfilled, await gives you the value. If it is rejected, it throws an error which you can catch using try catch.

Example:

try {
  const result = await promise;
  console.log(result);
} catch (error) {
  console.log(error);
}

So here await is receiving what resolve gives, and catch block is receiving what reject gives.

Now coming to promise lifecycle. A promise starts with pending. Then based on what happens, it moves to either fulfilled or rejected. It cannot go back once it is settled. So flow is simple. Pending to fulfilled or pending to rejected.

Promise chaining :

This is actually useful when you have multiple asynchronous steps.

Instead of writing everything inside one then, you can chain multiple thens.

Something like this:

fetchData()
  .then(data => {
    return processData(data);
  })
  .then(processed => {
    return saveData(processed);
  })
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.log(error);
  });

So what is happening here is each then returns something, and that becomes the input for the next then. This avoids nesting and keeps things readable.

Now just to connect it back to the original problem. Callbacks were doing the same thing but they were getting nested and hard to read. Promises solve that by giving a structured way to handle asynchronous operations. Better readability, better control over success and failure, and easier chaining.

If you look at it overall, promise is not doing something magical. It is just giving a cleaner way to deal with future values. Once you understand pending, fulfilled, rejected and how resolve and reject work, everything else starts falling into place.

And once you start using async and await on top of this, things become even more readable, almost like synchronous code even though it is asynchronous underneath.