Javascript: Promises Explained With Simple Real Life Examples

by admin admin Date: 06-05-2020 javascript coding promise tutorial


Handling asynchronous data flows is complex, who hasn't faced codes like this one?:

checkWeather('palma de mallorca', (error, weather) => {  	
   if (error) throw error;    	
   if (weather === 'well') {  		
   return checkFlights('palma de mallorca', (err, flights) => {  			
   if (err) throw err;    			

    buyTicket(flights[0], (e, ticket) => {  				
     if (e) throw e;  				
     console.log('ticket nº %d', ticket.number);  			
    });  		
  });  
 }    	
console.log('the weather is bad');  
});

This is known as callback hell, a complex code that is difficult to maintain, but there is a solution, Promises.

The previous code with promises would be something similar to this:

checkWatcher('palma de mallorca')
	.then(weather => {
		if (weather === 'well') {
			return checkFlights('palma de mallorca');
		}
		throw new Error('the weather is bad');
	})
	.then(flights => buyTicket(flights[0]))
	.then(ticket => {
		console.log('ticket nº %d', ticket.number);
	})
	.catch(error => console.error(error));

Fewer lines and much less indentation in the code, but let's see how a promise works with another example.

Suppose we go to buy food at a fast food restaurant, when we pay we get a ticket with an order number. Then, when that number is called we can go and get our food.

That ticket they gave us is our promise, that ticket tells us that we will be eventually getting our food, but that we don't have it yet. When they call that number to go get the food, it means that the promise is complete. But it turns out that a promise can be completed correctly or a mistake can occur.

For example, it may happen that the restaurant does not have the food we ordered so, when they call us with our number, two things may happen:

1. Our order is resolved and we get the food.
2. Our order is rejected and we get a reason why.

Let's put this in code:

const ticket = getFood();

ticket
	.then(food => eatFood(food))
	.catch(error => getRefund(error));

When we try to get the food (getFood) we get a promise (ticket), if this is resolved correctly then we get our food and eat it (eatFood). If our order is rejected then we get the reason (error) and ask for a refund (getRefund).

Creating a Promise

Promises are created by using a constructor called Promise and passing it a function that receives two parameters, resolve and reject, which allow us to indicate to it that it was resolved or rejected.

const promise = new Promise((resolve, reject) => {
	const number = Math.floor(Math.random() * 10);

	setTimeout(
		() => number > 5
			? resolve(number)
			: reject(new Error('less than 5')),
		1000
	);
});

promise
	.then(number => console.log(number))
	.catch(error => console.error(error));

What we have just done is to create a new promise that will be completed after 1 second, if the random number we generate is greater than 5 then it is resolved, if it is less than 5 then it is rejected and we get an error.

Promise states

This leads us to talk about the state of a promise, basically there are 3 possible states.

1. Pending
2. Resolved
3. Rejected

One promise is originally Pending. When we call resolve then the promise becomes Resolved, if we call reject it becomes Rejected, usually when it is rejected we get an error that will indicate the reason for the rejection. When a promise is resolved then the function we pass to the .then method is executed, if the promise is rejected then the function we pass to .catch is executed, this way we can control the data flow.

It is also possible to pass a second function to .then which would be executed in case of an error instead of executing the .catch

Receiving parameters

Before we create a promise, that promise is completed after 1 second and is resolved if the number generated is greater than 5. The solution is very simple, we create a function that receives the necessary parameters and returns the Promise instance.

function randomDelayed(max = 10, expected = 5, delay =  1000) {
	return new Promise((resolve, reject) => {
		const number = Math.floor(
			Math.random() * max)
		);

		setTimeout(
			() => number > expected
				? resolve(number)
				: reject(new Error('lower than expected number'));
			delay
		);
	});
}

randomDelayed(100, 75, 2500)
    .then(number => console.log(number))
    .catch(error => console.error(error));

When we execute randomDelayed(100, 75, 2500) we create a promise that after 2.5 seconds will be resolved whenever the generated number (between 0 and 100) is greater than 75. The same thing we had done before, but this time it is customizable.

Going from callback to promises

What if a function we want uses callbacks? How could we use it with promises? Very simply, we can create a promise version of that function by doing what we did above. For example reading a file using the fs module of Node.js.

import fs from 'fs';

function readFile(path) {
	return new Promise((resolve, reject) => {
		fs.readFile(path, 'utf8', (error, data) => {
			if (error) return reject(error);
			return resolve(data);
		});
	});
}

readFile('./file.txt')
	.then(data => console.log(data))
	.catch(error => console.error(error));

This way we create a function that reads a file from the disk like utf8 and if no error occurs then it is resolved, if there is an error it is rejected.

Chaining promises

In the first example of promises we saw something very interesting, we used many .then and called several functions that return promises. This pattern is called promise chaining.

Basically it prevents us from nesting code, instead one promise can return another promise and call the next .then in the chain. Let's see an example, suppose a .txt file returns a string with the path of another file, and we want to read this second file, with callbacks it would look something like this:

fs.readFile('./file.txt', 'utf8', (error, path) => {
	if (error) throw error;
	fs.readFile(path, 'utf8', (err, data) => {
		console.log(data);
	});
});

As we see inside our first callback we have to validate the first error, then call another function that gets the real data, and if we have to go nesting many functions that use callback we can get many levels of indentation. With promises this would be like this:

readFile('./file.txt')
	.then(readFile)
	.then(data => console.log(data))
	.catch(error => console.error(error));

What we do here, first we read ./file.txt, if an error occurs this promise is rejected and we show it in the console.error, if everything goes well the first .then is executed, this executes a new readFile, as .then receives the path to the new file and readFile only receives an argument (the path) then we can pass directly readFile and the promise is in charge of executing it.

This second readFile returns a new promise, again if there is an error the .catch is executed, but if we can read the file without problem then the second .then is executed, which receives the content of the second file and shows it in the console.

As we can see, we can simply chain as many .then as we want and keep running functions that return promises.

What's the best part?

It's not just promises that need to be returned, but if the function we pass to .then makes a return then the returned value is passed to the next .then in the string, no matter if it is a promise, an object, a string, a number or any other kind of data.

For example

import { resolve } from 'path';

readFile('./file.txt.')
	.then(resolve)
	.then(readFile)
	.then(data => console.log(data))
	.catch(error => console.error(error));

The function resolve that exports the path module allows us to set the absolute path to a file and returns a string, thanks to the .then we can do a function that receives the fileName of the first file and then returns this absolute path which arrives as a parameter to the second .then, which uses that path to read the second file and returns a promise that is resolved with the content of this one.

And as always, if at any moment there is an error, the .catch is executed.

Parallel Promises

So far we have only seen how to execute one asynchronous function at a time (in series), however it is very common that we need to perform multiple at once, for example to obtain several data from an API. For that the Promise class has a static method called Promise.all  which receives a single parameter, a list of promises which are executed simultaneously, if any of these are rejected then the whole list is, but if all are resolved then we can get a list of all the answers.

import { resolve } from 'path';

Promise.all([readFile('./file1.txt'), readFile('./file2.txt')])
	.then(data => data.map(resolve))
	.then(data => Promise.all(data.map(readFile)))
	.then(finalData => console.log(finalData))
	.catch(error => console.error(error));

What we do in the example above is read 2 files at a time, that returns a list (data) of contents, which contains the path for another file, we then convert them to a new list of absolute paths (resolve) and use those paths to create a new list of promises from readFile. If at any time an error occurred we show it as such in the console, if everything is resolved well then we write in the console the list of file contents.

Race of Promises

Before we talked about executing several promises in parallel and getting an answer when they are all completed, there is another method that allows us to run several at once, but only get the result of the first promise. This makes it possible to send multiple HTTP requests to an API and then receive a single response, the first one. This method is called Promise.race.

import { resolve } from 'path';

Promise.race([readFile('./file1.txt'), readFile('./file2.txt'])
	.then(resolve)
	.then(readFile)
	.then(data => console.log(data)
	.catch(error => consol.error(error));

As we see in the example again we read 2 files, but this time we only get the content of 1, the one that is read first. Or if one of them was completed with an error then we enter the catch and show the error in the console.

Promises resolved immediately

Sometimes the way to handle the data flow of the promises chaining then makes it easier for us to work with our code, so we can create a promise that starts directly resolved using a static Promise method.

Promise.resolve()
	.then(() => {
		// here we can do whatever we want
	});

Another option is to pass a parameter to resolve so that our first then receives that value.

import { resolve } from 'path';

Promise.resolve('./file1.txt')
	.then(resolve)
	.then(readFile)
	.then(data => console.log(data))
	.catch(error => console.error(error));

As we see we start the string with a string and from there we get the absolute path, read the file and show it in the console.

Promises rejected immediately

Just as we create promises resolved immediately we can create promises rejected. Only this time we use Promise.reject.

Promise.reject(new Error('our error'))
	.then(() => {
		// this function never runs
	})
	.catch(error => console.error(error));

What is this for? If we have a synchronous function that we want to solve by promises we could if it gives an error return Promise.reject with the error and Promise.resolve with the correct answer. This way, instead of creating an instance of the Promise class we simply return the promise already resolved or already rejected.

Conclusions

Working with promises makes it much easier for us to control asynchronous data flows in an application, plus promises are the basis for later implementing more advanced JavaScript features like Async/Await that make our code even easier.

 
by admin admin Date: 06-05-2020 javascript coding promise tutorial hits : 3654  
 
 
 
 

Related Posts