DEV Community

Cover image for The Story Behind JavaScript Promises - A Use Case Based Approach!
MirAli Mobasheri
MirAli Mobasheri

Posted on

The Story Behind JavaScript Promises - A Use Case Based Approach!

Table Of Contents:


The young librarian felt satisfied with what she did. Sarah had started her job when she was only 22 years old. Having known her as a long-time member of the library, the kind Mrs. Perkins hired her.

Mrs. Perkins had been the library's manager for as long as 30 years.

Now Sarah was the main person responsible for the place. She did a great job. People liked how she treated them and controlled everything.

But it didn't take long before things got out of control.

Mrs. Perkins retired. The girl had to do everything alone. And a new wave of readers was joining the library. She couldn't deliver services anymore.

She was forced to make promises...


❓ Why should you read this?

This might seem a long article. And the story might look unrelated.

But let's face the truth. How many times have you learned a concept but have never used it?
Perhaps there were situations where you could use Promise.race. But you didn't. Because despite you knew what it did, you weren't sure what its use case could be.

And when you got into a problem which you could solve using Promice.race, you chose to go your own way. Because you weren't even sure if it was useful there.

You knew what it did. But you didn't know its use case.

Here, I'm presenting you with a story. It also teaches you about different ways you can use Promises.

But it also tries to symbolize its use cases through a real-world story. Next time you face a similar situation you'll be quick to know how to handle it. And if not, you have at least read a story!πŸ˜‰

Enough talk. I have promises to keep.


It started with the Covid-19 pandemic outbreak. Everyone wanted to pass their quarantine by reading books. But no one could go to the library due to the health restrictions. They had to keep social distance.

Then Sarah came up with the idea to make an online deliverance system. Anyone could use the library's website to reserve books.

She thought that she could deliver the books on her bike to their door front. And as the transportation cost her time and money, she took a very small amount of dollars for her service.
But she wasn't always online and couldn't plan every books' deliverance on time. She had to deliver them herself and close the library since no one took her place.

She needed extra help. Somehow.


πŸ‡ In a hurry?

I have separated everything into different parts. Each of them is about a specific side of the Promises. So you can skip any part you are confident about.

Already know how to make JavaScript Promises? Skip the introduction part!


She called her cousin and asked him if he could design an ordering system for the library's website. She described the process as: "People will have to log in to their accounts. And if there is a book they want to request, they should put an online order ticket for it."

But since it was not clear when she could deliver the books, the users had to wait for the website to notify them.
It sent a message. If the book was available to deliver, it informed them she had resolved the ticket. Otherwise, it contained a rejection message.

She named it the Promise System. Likewise the JavaScript API we're going to learn about.

Let's dive in!


🀝 How Promises Are Made: Please Put A Ticket!

To create a JavaScript Promise you can use the new keyword:

const thePromise = new Promise()
Enter fullscreen mode Exit fullscreen mode

The Promise Object Constructor accepts a function as an argument. This function runs the process that we're going to promise its fulfillment.

In our story, the way Sarah manages the ordering through tickets resembles such a Promise. Either it is fulfilled with the book's delivery or rejected for a reason.

This function can in turn accept two arguments: resolve and reject.
Both of which are callback functions that we can delegate on certain points.

We use resolve as a fulfillment signal and pass it the final data. By calling reject we make clear that the Promise has failed.

From what we've learned we can construct a Promise by writing the following lines:

const theOrderingProcess = (resolve, reject) => {
  // The process with the unknown duration.
  // Function body goes here.

  // If anywhere in your code, 
  // you think your promise is fulfilled, 
  // (e.g. Sarah finds an ordered book is available),
  // you can call resolve and pass it 
  // the data you would like to return. 
  // Like the following:
  // resolve(theBookData)

  // But if somehow the promise has failed to fulfill, 
  // (e.g. A book is currently borrowed),
  // you can call reject and
  // pass it the reason to rejection:
  // reject(theRejectionReason)
}
const theWebsitePromise = new Promise(theOrdeingProcess)
Enter fullscreen mode Exit fullscreen mode

Every time someone put a ticket for a book, he/she had to wait for a certain process to come to an end.
It was not as if you selected a book and immediately had it on way to your home. Things took time. How long? No one knew for granted.

Mrs. Perkins wasn't an all-up-to-date lady. So she had stored most of the data about the existing books and the borrowed ones in an old Access database. It took time to transfer them to a server. And Sarah wanted things to get done as soon as possible.

She had to use the Access database. By herself.


πŸ”› Main Usages Of A Promise: Making API Calls.

Nowadays most websites use a database on their back end side. Like how Mrs. Perkins stored the library's data in an Access database.

Of course the website databases use automations!

A web application's front end needs to send a request to a server endpoint to receive some data. Then using this data the application can deliver its services.

As the front end sends the request, the process mounts into a pending status. It will stay in this status until it either receives a response from the back end or receives none.

The pending status could take for an unknown period. This depends on the networking conditions of the user and the server, and how fast they can transfer data. Additionally, the back end itself might need to process some data and take extra time.

An example of sending a request using the JavaScript Fetch API looks like the following:

const theRequest = fetch("https://example.com/endpoint")
Enter fullscreen mode Exit fullscreen mode

The fetch method constructs a Promise Object, and returns it. So we can use its returned value same as a simple Promise Object. Helping us to get rid of creating our own Promisess.

Want more comfort? Use axios.


The library's online ordering system took tickets from its users. They contained information about the user and the selected book. Sarah reviewed the tickets. And checked for the books' availability in the Access database.

If everything was okay, a green button labeled as resolve, awaited Sarah's click. Otherwise, the big red button was ready to delegate a rejection event.

No one knows for sure. But rumors are there was a black button too. For dark purposes. Unknown.


βœ…βŒ Callbacks: The Green And The Red Button.

We call the processes like the API requests Asynchronous Operations. They vary from the Synchronous ones which are simple Program Expressions.
A Synchronous Operation takes a constant time to complete. The system runs them in a sequence. It waits until the execution of one is complete before it runs the next.

A Synchronous Operation looks like the following:

const x = 10;
const z = function(num1, num2) {
  return num1 + num2;
}
const sum = z(x, 20);
Enter fullscreen mode Exit fullscreen mode

Every line of this operation is predictable. The compiler will be executing each line one after another. There's nothing unpredictable that could block the program's main thread from running.

But an Asynchronous Operation can block the program's execution. This is so because its fulfillment depends on the network conditions and speed. Even if the user owned a fast network, the back-end servers could be facing trouble. This can result in no response or a longer-running process.

How can we write an Asynchronous Operation and make sure that it doesn't block the rest of the code from running?

The answer is: "by using Callbacks".

Of course, while it sounds like a heartwarming name, wait until you are caught in a callback hell!


When the user put a ticket for a book, he registered for future events. He was awaiting a response to his request.

Either this response arrived, or some problem prevented it from ever coming.

Now the user was using a true Promise System. The library's website provided this system. But the user didn't have direct access to the promise. He was listening to it.

Then the user had to make his own plans based on this Promise. He was telling himself: if the reply was a successful message then I'm gonna return my last book. If not, then I can choose another one. Maybe the reply never came, or the website's server went down. This problem will catch his eyes, and so he was going to call the librarian and inform her.

console.warn('I never got a response!').


↩️β†ͺ️ Callbacks: Then & Catch.

If you use the JavaScript Fetch API, it will return a Promise Object by default. You don't have to write the Promise yourself. You have to listen to it. How?

Every Promise returns an Object. And this Object owns 2 important methods. then and catch. You can use these methods to attach callbacks to a Promise. You pass them a function which will be called as soon as the Promise delegates an event. What events?

You can call the resolve callback inside a Promise Function's body and pass it what data you want. Then the Promise calls the callback function inside the first then call and passes the data to it. The Promise calls the catch method as soon as you call the rejection callback.

Let's visualize this process by an example:

const request = fetch("https://example.com/endpoint")
request
  .then((data) => data.json())
  .catch((error) => console.log("Error in fetching the request", error))
Enter fullscreen mode Exit fullscreen mode
  • Here we call the fetch function and pass it the endpoint URL.
  • The fetch function creates a Promise Object and returns it.
  • We store this Object in the request variable.
  • We call request's then method and pass it a function. The function can receive as many arguments as it expects. It retrieves those arguments from what the Promise passes to the resolve callback. Then the given function can do as much as it desires with the given data.
  • Next, we call request's catch method. We pass it a function. And expect the Promise to call this function when it rejects or the then method throws an Error.

  • In the end we run the code to see if it works. Of course it does. And if not, we won't hesitate to point our finger at the back end:)

A question exists. The then and catch methods only add callbacks to the Promise Object. So why aren't they properties to which we can attach the callback functions? Wouldn't it be simpler?

We'll find the answer.


Everything Sarah did resulted in a Promise.
Hitting the resolve button was only the first step.

She had to find the book in the library. She had to package it next to the other ones which she had to deliver the following day.

She had to mark each package with the right user's information. This included their names and addresses. She had to be careful when she loaded the packages on the bike. Not to disturb her ride.

She had to ride through streets and alleys. Dodging traffics and caring for her safety. Reaching her destination, she had to ring the doorbell. If the client was present, then she delivered the book.
And then she took the reader's previous book to return it to the library.

After repeating the same routine for every client, she had to finally go back to the library. She placed the returned books on the shelves. Then filled the Access database with data about the returned and the delivered ones.

After all, it was time to review that day's orders and check for their availability. Then managing to hit the resolve or the reject buttons.

Even the rest she got by her night sleeps, was a promise she had made to her body.

And it feels good to fulfill promises. Doesn't it?😊


⛓️ The Methodology Behind Promises: Returning Objects!

We've faced a question. A matter which insists that providing callbacks through methods is useless. The Promise API can instead provide special Object Properties. Or the ability to add an array of callbacks, for another instance.

But think of all the possibilities you might face while developing front-end applications. You won't always want the callbacks to run in order. You might not remember a case for this at the moment, but sure someday you'll face it.

Different scenarios need different approaches. And if the Promise is going to be of any help during these situations, it has to be flexible.

Let's have a look at a piece of code:

const aPromise = Promise.resolve("resolved")
aPromise.then(res => console.log(res))
Enter fullscreen mode Exit fullscreen mode

Here we have been able to use the Promise's static resolve method. Without even having to construct a new Promise Instance we were able to return a Promise Object.

Now we can call this Object's then method. Or its catch one.

Well, what good is that for? You may ask. To find the answer let's have a look at another example:

const firstPromise = Promise.resolve({message: "hello"})

const secondPromise = firstPromise
  .then(data => JSON.stringify(data))
  .then(json => json.indexOf(message) !== -1)
  .then(boolean => {
    if(boolean) console.log("JSON string was created successfuly!")
    else throw new Error("JSON creation error")
  })
  .catch(error => console.error(error))

const thirdPromise = firstPromise
  .then(data => {
    if(data.message) return data.message + " world!";
    else throw new Error("The data doesn't contain a message!")
  })
  .then(newData => console.log(newData.toUpperCase()))
  .catch(error => console.error("Error in third Promise", error))
Enter fullscreen mode Exit fullscreen mode

Here, we've initiated a Promise Object using only Promise.resolve. This promise object gives us all the superpowers we want. We can call its then method as much as we want, in a chain or in separate calls. This allows us to create various waiters for the same process. What is a waiter?

Let's say you have got to fetch data, which you will use in different parts of your application. But there's a problem. You can reuse synchronous operation data as in variables. But you can't do that with asynchronous data. You should wait for it to become available.

Now, there are different parts in your project, awaiting this data. We can refer to these parts as waiters. They're observing the Promise's status and as soon it settles, they will read its data.

The above example showcases how the then and catch methods help you to reuse the same data in different scenarios.

This way the Promise API is providing an easy-to-use solution.

But it also provides us with some useful static methods. We can use them to handle different situations. They include: Promise.all, Promise.allSettled, Promise.any, and Promise.race.


Every day, to return to the library Sarah had to make sure that she had visited every one of her destinations. If all the clients were present at home, the book exchanges were successful. Otherwise, some of them failed.

She didn't need every deliverance to be resolved. She needed to have finished the process which was about going to the client's house.

In the end, she returned to the library. And when she was ready to enter the reports in the Access database, she would ask herself: "Was everyone at home? Did any of the orders remain undelivered?"

She classified the undelivered books in a separate database. And she sent the clients an Email that expressed the situation.

If she had delivered every book then, she only had to set that day's deliveries as done, in the database. Nothing further to take care of.


πŸ•ΈοΈ Static Methods: Promise.all & Promise.allSettled

The all method accepts an array of promises. Then it waits for all the promises to resolve. If any of the promises rejects, it will immediately return with the rejection reason.

This function behaves like what Sarah asks herself every night: 'Was everyone at home? Did any of the orders remain undelivered?'

She will know that she has done all her tasks once she delivered the last book. If so, only the clients' returned books would be on her bike.

Promise.all resolves once every Promise in the promises array passed to it resolves. It returns an array of what each promise had returned. Like the books that the library's clients returned.

Sarah immediately knew that the delivery has failed If none of the clients were present at home. She would return with the undelivered book still on her bike.

If any of the promises you pass to Promise.all rejects, it will immediately reject. With the rejection reason of the rejected promise.

An example of Promise.all usage:

Promise.all([client1, client2, client3])
  .then(returnedBooks => console.log(returnedBooks))
  .catch(rejectionReason => console.log("Error: a book was not delivered.", rejectionReason))
Enter fullscreen mode Exit fullscreen mode

We said that Sarah's return to the library didn't depend on every client's presence. She needed to make the visit. So if she had toured around all the clients' addresses, she was ready to return to the library. And on her bike, she transported all the books. Whether returned or undelivered.

For a likewise behavior with JavaScript Promises, we can use Promise.allSettled.

Every Promise passes through two main states: pending and settled. The settled state is when the promise has been fulfilled. The settlement either happens with resolve or a rejection.

The all method immediately returned when a Promise rejected or every promise resolved.
But the allSettled method immediately returns. When all promises have either resolved or rejected.

When they are settled indeed.

What the allSettled method returns in its resolve, consists of an array of objects. Each object has a status property which is either fulfilled or rejected.

If the status is fulfilled then the object provides a value property too. Otherwise, it owns a reason property.

A demonstration of it in code:

Promise.allSettled([client1, client2, client3])
  .then(books => 
    books.forEach(book => {
      if(book.status === "fulfilled") {
        console.log("A returned book", book.value)
      } else {
        console.log("An undelivered book", book.reason)
      }
    })
  )
Enter fullscreen mode Exit fullscreen mode

Soon enough troubles showed up.

The website's users had to wait a long time for the response message's arrival. Then they started to see more rejection messages. No matter what book they ordered, a rejection message awaited them in their inboxes after 1 to 2 days.

Some of the users tried to contact Sarah through Emails. But they only got a simple reply. It stated: "The requested book is currently borrowed by someone else. You can try to order it later, or borrow another one."

These replies worsened the situation. It surprised the clients that Sarah didn't even state when the book will be available. Or whether she could put them in a queue.

Everything seemed unexpected and random.

Sarah at first didn't notice any issue. She was still taking books for some people. But later it caught her eyes that things were getting weird.

Before anything started to happen she wanted to find a solution to speed her routine up. She called her cousin and asked him if he could help her in searching the current Access databases. She needed to be quicker in finding out if a book was available.

He said he would look into it and call her as soon as he found anything. The next day he had a solution. He could write a python bot which would search every Access database. And to speed things up, he had found a useful algorithm for it too.

He made the bot ready within a day. It searched the Access files for a specified book name and stated whether it was available to borrow. His special algorithm indeed created several asynchronous operations.

It searched each file in real-time with the others. As soon as any of the files contained a search result matching the book, the bot took it. Then it terminated all the search operations.

She had separated the Access files into two different directories. She named one of them the library books and the other the ordered books.

Books in the ordered books' directory were currently ordered or delivered.

The library books databases had a list of all books in the library. It also contained information on whether a book was available or not.

While developing his bot he had decided that the result was either in the ordered books databases or the library ones. If it was the ordered ones then it was obvious that the book wasn't available and Sarah had to reject the ticket.
Else if it found the result in the library databases it was either available or not.

This speed the search up because the ordered databases were smaller than the other group. So if it found a match result among them it terminated the searching quicker.

But two weeks after the new bot's implementation most of the orders had to be rejected. She had had a regular delivery count of 9 to 10 books per day. Now, this number had fallen to as low as 3.

Some days none at all.

Many of the borrowed books remained unreturned for a long time. Their owners didn't even order new ones.

And then she heard rumors. One of her best friends had tested positive for Covid-19. The infection statistics were surging in her town. She called some of her customers to ask why they weren't trying to return their books.

Her suspicions proved right. Covid-19 had hit many of her customers. But some other ones were actually bothered with the site's functionality. They hated waiting a long time for no results.

She tried to talk them back into using the library's website. She even promised that she will fix the issues. But promises weren't convincing anymore.

One night she called her cousin. Tears in her eyes. "I failed.", she said. The confession feeling bitter. "I failed badly."


πŸƒπŸ»β€β™‚οΈ Static Methods: Promise.any & Promise.race.

Promise.any accepts an array of promises. And resolves as soon as any of them fulfills.

The library's users chose a book and ordered it and waited to see if it was available. If not, they would go for the next one. But this process took a long time. They had to wait for each order's message to arrive before they could decide anything.

And when the system became unresponsive it wasted the users' precious time by days and weeks.

What if they could find a way to order many books together and get the first book that was available? Of course, it was impossible with the library website's architecture. Sarah wouldn't have allowed this. This could ruin all her plans.

But this is not the case with JavaScript. If you are waiting for the first promise that resolves, use Promise.any. It works the same way as the hack the library users wished for.

An implementation of Promise.any:

Promise.any([order1, order2, order3])
  .then(order => console.log("this order resolved first", order))
  .catch(error => console.log(error)
    // expected output: "AggregateError: No Promise in Promise.any was resolved"
  )
Enter fullscreen mode Exit fullscreen mode

As soon as any of the promises resolves, the Promise Object calls the callback passed to then. If all reject, it enters the catch territory.

Then there is Promise.race. It resolves with the first Promise that settles. In case you've already forgotten, a Settled Promise is either fulfilled or rejected.

Now, why should someone need to implement that? It's actually difficult to name a good use case for Promise.race. But still, there are few times when it could be of help.

Let's think about the bot that Sarah's cousin had developed. It exited the processes as soon as any of them returned anything. You might say that the bot's actual action is like what Promise.any does. It returns as one of them fulfills.

Then it can be a bad example. But let's say that the Promise which he used to search the ordered books didn't fulfill when it matched a book. Let's say Sarah's cousin was smarter and played a trick on it.

If the search in the ordered books matched a result then its Promise rejected. Otherwise, if it had searched the entire databases and found none it resolved. But, if the search process related to the library books found a match and it was available then it resolved. Otherwise, the Promise rejected.

So here we have got a bright point. The rejection of one of the promises means the resolving of the other one. And vice versa.

Now, Promise.race can help us in reducing the time we need to wait for a useful response.

Let's have a quick look:

Promise.race([searchLibraryBooks, searchOrderedBooks])
  .then((book) => console.log("Book is available."))
  .catch((reason) => console.log("Book is not available, because ", reason))
Enter fullscreen mode Exit fullscreen mode

The library books have an alphabetical sorting. The ordered ones are sorted by their order date. So it's possible that the search in the whole library books could make a quicker match sometimes.


It didn't take long before Sarah received a phone call from his cousin. He sounded anxious. And when she asked him what was wrong, he replied: "The bot had a bug, Sarah. There are always several versions of a book in the library. This means that if someone borrows one of its versions the book could still be available. I hadn't thought about this in my code. If one version of the book was ordered then I tagged it as unavailable. I'm sorry Sarah."

Sarah was in shock for several seconds. How didn't this even cross her own mind?
"Can you fix it?", she asked.
"Well I'll do my best.", the other replied.
"Then you better do it.", she couldn't control how loud her voice got. "And do it as soon as possible!"

It took another day for them to fix the issue. She sent Emails to her clients. Informing that they have fixed the issue. And the users could start using the website immediately.


πŸ”š Promise Callbacks: finally.

We learned about then and catch. But the Promise Object also provides us with another method. It doesn't care if the Promise fulfills or not. It only cares that it settles. And any then and catch calls have already been delegated. Then it runs and does what you ask it for.

An example:

const websiteDelivery = Promise.resolve({useServerDatabase: false})
websiteDelivery.then(condition => {
    if(!condition.useServerDatabase) console.log('Use Access database')
    else throw new Error('Data isn't still moved to the server')
  )
  .catch(error => console.log("Error in starting delivery website", error))
  .finally(_ => console.log("No matter if you lost 100 times. Try for the 101st time!")
Enter fullscreen mode Exit fullscreen mode

The Promise Object calls the function you passed to the finally method in any condition. After the Promise settles.


"The devil is always in the details." This was what Mrs. Perkins told Sarah after she heard her story. She went on: "You saw the big picture. You wanted to start a movement and to modernize our town's library. But you forgot the details my dear. You told yourself that you can handle the database work. But this was something you had to leave to the programmer. After you've finally restored the library's daily life, I hope you've learned something. And I hope that this damned pandemic ends very soon."

Sarah hoped too. And she smiled.


It was a long read. And a longer one to write. I think it was worth the time.

The hardest part was about Promise.race. I could hardly find a good use case for it. And I hope the idea I came up with could help you make a picture of it.

Do you think there are better use cases for any of these methods? Or do you think I have gotten something wrong?

Please let me know in the comments. Writing this article helped me a lot in understanding the concepts myself. Your comments can help me and the others further too.

Thanks for the reading.

The cover image is by Epic Top 10 Site.

Top comments (1)

Collapse
 
fuhadihthisham profile image
fuhad ihthisham

You explained it very nicely, thanks bro.