Home » How JavaScript Promises Work – Handbook for Newbies

How JavaScript Promises Work – Handbook for Newbies

by Icecream
0 comment

Many operations, akin to community requests, are asynchronous in nature. One of probably the most helpful and highly effective instruments for working with asynchronous code is the Promise. In this handbook, you will study all about JavaScript Promises and methods to use them.

Table of Contents

  1. What is a Promise?
  2. Comparing Promises to Other Async Patterns
  3. How to Create a Promise
  4. How to Get the Result of a Promise
  5. How to Handle Errors with then
  6. Promise Chaining
  7. How to Create Immediately Fulfilled or Rejected Promises
  8. How to Use async and await
  9. Promise Anti-Patterns
  10. Summary

What is a Promise?

Let’s start by taking a look at what a Promise is.

In easy phrases, a Promise is an object representing an asynchronous operation. This object can inform you when the operation succeeds, or when it fails.

When you name a Promise-based API, the operate returns a Promise object that can ultimately present the results of the operation.

Promise states

During its lifetime, a Promise may be in one in all three states:

  • Pending: A Promise is pending whereas the operation remains to be in progress. It’s in an idle state, ready for the eventual end result (or error).
  • Fulfilled: The asynchronous job that returned the Promise accomplished efficiently. A Promise is fulfilled with a price, which is the results of the operation.
  • Rejected: If the asynchronous operation failed, the Promise is alleged to be rejected. A Promise is rejected with a motive. This usually is an Error object, however a Promise may be rejected with any worth – even a easy quantity or string!

A Promise begins out within the pending state, then relying on the end result, will transition to both the fulfilled or rejected state. A Promise is alleged to be settled as soon as it reaches both the fulfilled or rejected state.

Of course, there isn’t any assure that the asynchronous job will ever full. It’s utterly attainable for a Promise to stay within the pending state ceaselessly, although this is able to be due to a bug within the asynchronous job’s code.

Comparing Promises to Other Async Patterns

Promises behave a little bit in another way from different asynchronous patterns in JavaScript. Before diving deeper into Promises, let’s briefly examine Promises to those different methods.

Callback features

A callback operate is a operate that you simply cross to a different operate. When the operate you name has completed its work, it is going to execute your callback operate with the end result.

Imagine a operate referred to as getUsers which is able to make a community request to get an array of customers. You can cross a callback operate to getUsers, which shall be referred to as with the array of customers as soon as the community request is full:

console.log('Preparing to get customers');
getUsers(customers => {
  console.log('Got customers:', customers);
});
console.log('Users request despatched');
An instance of a callback operate

First, the above code will print “Preparing to get customers”. Then it calls getUsers which is able to provoke the community request. But JavaScript would not look ahead to the request to finish. Instead, it instantly executes the subsequent console.log assertion.

Later, as soon as the customers have been loaded, your callback shall be executed and “Got customers” shall be printed.

Some callback-based APIs, akin to many Node.js APIs, use error-first callbacks. These callback features take two arguments. The first argument is an error, and the second is the end result.

Typically, solely one in all these may have a price, relying on the result of the operation. This is just like the fulfilled and rejected Promise states.

The bother with callback APIs is that of nesting. If you could make a number of asynchronous calls in sequence, you’ll find yourself with nested operate calls and callbacks.

Imagine you need to learn a file, course of some knowledge from that file, then write a brand new file. All three of those duties are asynchronous and use an imaginary callback based mostly API.

readFile('sourceData.json', knowledge => {
	processData(knowledge, end result => {
		writeFile(end result, 'processedData.json', () => {
			console.log('Done processing');
		});
	});
});
A sequence of nested callbacks

It will get much more unwieldy with error dealing with. Imagine these features used error-first callbacks:

readFile('sourceData.json', (error, knowledge) => {
	if (error) {
		console.error('Error studying file:', error);
		return;
	}
	
	processData(knowledge, (error, end result) => {
		if (error) {
			console.error('Error processing knowledge:', error);
			return;
		}
		
		writeFile(end result, 'processedData.json', error => {
			if (error) {
				console.error('Error writing file:', error);
				return;
			}
			
			console.log('Done processing');
		});
	});
});
A sequence of nested error-first callbacks

Callback features aren’t usually used instantly as an asynchronous mechanism in fashionable APIs, however as you will quickly see, they’re the inspiration for different kinds of asynchronous instruments akin to Promises.

Events

An occasion is one thing which you can hear for and reply to. Some objects in JavaScript are occasion emitters, which suggests you’ll be able to register occasion listeners on them.

In the DOM, many components implement the EventTarget interface which supplies addEventListener and removeEventListener strategies.

A given kind of occasion can happen greater than as soon as. For instance, you’ll be able to hear for the clicking occasion on a button:

myButton.addEventListener('click on', () => {
   console.log('button was clicked!'); 
});
Listening for a click on on a button

Every time the button is clicked, the textual content “button was clicked!” shall be printed to the console.

addEventListener itself accepts a callback operate. Whenever the occasion happens, the callback is executed.

An object can emit a number of kinds of occasions. Consider a picture object. If the picture on the specified URL is loaded efficiently, the load occasion is triggered. If there was an error, this occasion just isn’t triggered and as an alternative the error occasion is triggered.

myImage.addEventListener('load', () => {
    console.log('Image was loaded');
});

myImage.addEventListener('error', error => {
   console.error('Image didn't load:', error); 
});
Listening for a picture’s load and error occasions

Suppose the picture already accomplished loading earlier than you added the occasion listener. What do you assume would occur? Nothing! One downside of event-based APIs is that when you add an occasion listener after an occasion, your callback will not be executed. This is sensible, in any case – you would not need to obtain all previous click on occasions whenever you add a click on listener to a button.

Now that we have explored callbacks and occasions, let’s take a more in-depth take a look at Promises.

How to Create a Promise

You can create a Promise utilizing the new key phrase with the Promise constructor. The Promise constructor takes a callback operate that takes two arguments, referred to as resolve and reject. Each of those arguments is a operate supplied by the Promise, that are used to transition the Promise to both the fulfilled or rejected state.

Inside your callback, you carry out your asynchronous work. If the duty is profitable, you name the resolve operate with the ultimate end result. If there was an error, you name the reject operate with the error.

Here’s an instance of making a Promise that wraps the browser’s setTimeout operate:

operate wait(period) {
	return new Promise(resolve => {
        setTimeout(resolve, period);
    });
}
Wrapping setTimeout in a Promise

The resolve operate is handed as the primary argument to setTimeout. After the time specified by period has handed, the browser calls the resolve operate which fulfills the Promise.

Note: In this instance, the delay earlier than the resolve operate is known as could also be longer than the period handed to the operate. This is as a result of setTimeout doesn’t assure execution on the specified time.

It’s vital to notice that usually occasions, you will not really have to assemble your individual Promise by hand. You will usually be working with Promises returned by different APIs.

How to Get the Result of a Promise

We’ve seen methods to create a Promise, however how do you really get the results of the asynchronous operation? To do that, you name then on the Promise object itself. then takes a callback operate as its argument. When the Promise is fulfilled, the callback is executed with the end result.

Let’s see an instance of this in motion. Imagine a operate referred to as getUsers that asynchronously hundreds an inventory of person objects and returns a Promise. You can get the record of customers by calling then on the Promise returned by getUsers.

getUsers()
  .then(customers => {
    console.log('Got customers:', customers);
  });
Calling then on a Promise

Just like with occasions or callback based mostly APIs, your code will proceed executing with out ready for the end result. Some time later, when the customers have been loaded, your callback is scheduled for execution.

console.log('Loading customers');
getUsers()
  .then(customers => {
    console.log('Got customers:', customers);
  });
console.log('Continuing on');

In the above instance, “Loading customers” shall be printed first. The subsequent factor that’s printed shall be “Continuing on”, as a result of the getUsers name remains to be loading the customers. Later, you will see “Got customers” printed.

How to Handle Errors with then

We’ve seen methods to use then to get the end result supplied to the Promise, however what about errors? What occurs if we fail to load the person record?

The then operate really takes a second argument, one other callback. This is the error handler. If the Promise is rejected, this callback is executed with the rejection worth.

getUsers()
  .then(customers => {
    console.log('Got customers:', customers);
  }, error => {
    console.error('Failed to load customers:', error);  
  });

Since a Promise can solely ever be both fulfilled or rejected, however not each, solely one in all these callback features shall be executed.

It’s vital to all the time deal with errors when working with Promises. If you could have a Promise rejection that is not dealt with by an error callback, you will get an exception in your console about an unhandled rejection, which may trigger points in your customers at runtime.

Promise Chaining

What if you could work with a number of Promises in collection? Consider the sooner instance the place we loaded some knowledge from a file, did some processing, and wrote the end result to a brand new file. Suppose the readFile, processData, and writeFile features used Promises as an alternative of callbacks.

You may strive one thing like this:

readFile('sourceData.json')
  .then(knowledge => {
    processData(knowledge)
      .then(end result => {
        writeFile(end result, 'processedData.json')
          .then(() => {
            console.log('Done processing');
          });
      });
  });
Nested guarantees

This would not look nice, and we nonetheless have the nesting problem that we had with the callback strategy. Thankfully, there’s a higher means. You can chain Promises collectively in a flat sequence.

To see how this works, let’s look deeper at how then works. The key concept is that this: the then methodology returns one other Promise. Whatever worth you come out of your then callback turns into the fulfilled worth of this new Promise.

Consider a getUsers operate that returns a Promise that will get fulfilled with an array of person objects. Suppose we name then on this Promise, and within the callback, return the primary person within the array (customers[0]):

getUsers().then(customers => customers[0]);

This entire expression, then, ends in a brand new Promise that shall be fulfilled with the primary person object!

getUsers()
  .then(customers => customers[0])
  .then(firstUser => {
    console.log('First person:', firstUser.username);
  });
Returning a price from the then handler

This technique of returning a Promise, calling then, and returning one other worth, leading to one other Promise, is known as chaining.

Let’s prolong this concept. What if, as an alternative of returning a price from the then handler, we returned one other Promise? Consider once more the file-processing instance, the place readFile and processData are each asynchronous features that return Promises:

readFile('sourceData.json')
  .then(knowledge => processData(knowledge));
Returning one other Promise from then

The then handler calls processData, returning the ensuing Promise. As earlier than, this returns a brand new Promise. In this case, the brand new Promise will turn into fulfilled when the Promise returned by processData is fulfilled, giving you an identical worth. So the code within the above instance would return a Promise that shall be fulfilled with the processed knowledge.

You can chain a number of Promises, one after the opposite, till you get to the ultimate worth you want:

readFile('sourceData.json')
  .then(knowledge => processData(knowledge))
  .then(end result => writeFile(end result, 'processedData.json'))
  .then(() => console.log('Done processing'));
Chaining a number of guarantees

In the above instance, the entire expression will lead to a Promise that will not be fulfilled till after the processed knowledge is written to a file. “Done processing!” shall be printed to the console, after which the ultimate Promise will turn into fulfilled.

Error dealing with in Promise chains

In our file-processing instance, an error can happen at any stage within the course of. You can deal with an error from any step within the Promise chain by utilizing the Promise’s catch methodology.

readFile('sourceData.json')
  .then(knowledge => processData(knowledge))
  .then(end result => writeFile(end result, 'processedData.json'))
  .then(() => console.log('Done processing'))
  .catch(error => console.log('Error whereas processing:', error));
Handling errors with catch

If one of many Promises within the chain is rejected, the callback operate handed to catch will execute and the remainder of the chain is skipped.

How to make use of lastly

You may need some code you need to execute whatever the Promise end result. For instance, possibly you need to shut a database or a file.

openDatabase()
  .then(knowledge => processData(knowledge))
  .catch(error => console.error('Error'))
  .lastly(() => closeDatabase());

How to make use of Promise.all

Promise chains allow you to run a number of duties in sequence, however what if you wish to run a number of duties on the identical time, and wait till all of them full? The Promise.all methodology enables you to just do that.

Promise.all takes an array of Promises, and returns a brand new Promise. This Promise shall be fulfilled as soon as all the different Promises are fulfilled. The success worth is an array containing the success values of every Promise within the enter array.

Suppose you could have a operate loadUserProfile that hundreds a person’s profile knowledge, and  one other operate loadUserPosts that hundreds a person’s posts. They each take a person ID because the argument. There’s a 3rd operate, renderUserWeb page, that wants each the profile and record of posts.

const userId = 100;

const profilePromise = loadUserProfile(userId);
const postsPromise = loadUserPosts(userId);

Promise.all([profilePromise, postsPromise])
  .then(outcomes => {
    const [profile, posts] = outcomes;
    renderUserWeb page(profile, posts);
  });
Waiting for a number of guarantees with Promise.all

What about errors? If any of the Promises handed to Promise.all is rejected with an error, the ensuing Promise can also be rejected with that error. If any of the opposite Promises are fulfilled, these values are misplaced.

How to make use of Promise.allSettled

The Promise.allSettled methodology works equally to Promise.all. The foremost distinction is that the Promise returned by Promise.allSettled won’t ever be rejected.

Instead, it’s fulfilled with an array of objects, whose order corresponds to the order of the Promises within the enter array. Each object has a standing property which is both “fulfilled” or “rejected”, relying on the end result.

If standing is “fulfilled”, the article may also have a worth property indicating the Promise’s success worth. If standing is “rejected”, the article will as an alternative have a motive property which is the error or different object the Promise was rejected with.

Consider once more a getUser operate that takes a person ID and returns a Promise that’s fulfilled with the person having that ID. You can use Promise.allSettled to load these in parallel, ensuring to get all customers that had been loaded efficiently.

Promise.allSettled([
  getUser(1),
  getUser(2),
  getUser(3)
]).then(outcomes => {
   const customers = outcomes
     .filter(end result => end result.standing === 'fulfilled')
     .map(end result => end result.worth);
   console.log('Got customers:', customers);
});
Attempting to load three customers, and displaying those that had been efficiently loaded

You could make a basic objective loadUsers operate that hundreds customers, in parallel, given an array of person IDs. The operate returns a Promise that’s fulfilled with an array of all customers that had been efficiently loaded.

operate getUsers(userIds) {
  return Promise.allSettled(userIds.map(id => getUser(id)))
    .then(outcomes => {
      return outcomes
        .filter(end result => end result.standing === 'fulfilled')
        .map(end result => end result.worth);
    });
}
A helper operate to load a number of customers in parallel, filtering out any requests that failed.

Then, you’ll be able to simply name getUsers with an array of person IDs:

getUsers([1, 2, 3])
	.then(customers => console.log('Got customers:', customers));
Using the getUsers helper operate

Sometimes, chances are you’ll need to wrap a price in a fulfilled Promise. For instance, possibly you could have an asynchronous operate that returns a Promise, however there’s a base case the place you understand the worth forward of time and also you needn’t do any asynchronous work.

To do that, you’ll be able to name Promise.resolve with a price. This returns a Promise that’s instantly fulfilled with the worth you specified:

Promise.resolve('howdy')
  .then(end result => {
    console.log(end result); // prints "howdy"
  });
Using Promise.resolve

This is kind of equal to the next:

new Promise(resolve => {
   resolve('howdy'); 
}).then(end result => {
    console.log(end result); // additionally prints "howdy"
});

To make your API extra constant, you’ll be able to create an instantly fulfilled Promise and return that in such instances. This means, the code that calls your operate is aware of to all the time anticipate a Promise, it doesn’t matter what.

For instance, take into account the getUsers operate outlined earlier. If the array of person IDs is empty, you can merely return an empty array as a result of no customers shall be loaded.

operate getUsers(userIds) {
  // instantly return the empty array
  if (userIds.size === 0) {
    return Promise.resolve([]);
  }
    
  return Promise.allSettled(userIds.map(id => getUser(id)))
    .then(outcomes => {
      return outcomes
        .filter(end result => end result.standing === 'fulfilled')
        .map(end result => end result.worth);
    });
}
Adding an early return to the getUsers helper operate

Another use for Promise.resolve is to deal with the case the place you’re given a price that will or might not be a Promise, however you need to all the time deal with it as a Promise.

You can safely name Promise.resolve on any worth. If it was already a Promise, you will simply get one other Promise that can have the identical success or rejection worth. If it was not a Promise, it will likely be wrapped in an instantly fulfilled Promise.

The good thing about this strategy is you do not have to do one thing like this:

operate getResult(end result) {
  if (end result.then) {
     end result.then(worth => {
         console.log('Result:', worth);
     });
  } else {
      console.log('Result:', end result);
  }
}
Conditionally calling then based mostly on whether or not or not one thing is a Promise

Similarly, you’ll be able to create an instantly rejected Promise with Promise.reject. Returning as soon as once more to the getUsers operate, possibly we need to instantly reject if the person ID array is null, undefined, or not an array.

operate getUsers(userIds) {
  if (userIds == null || !Array.isArray(userIds)) {
    return Promise.reject(new Error('User IDs have to be an array'));
  }
    
  // instantly return the empty array
  if (userIds.size === 0) {
    return Promise.resolve([]);
  }
    
  return Promise.allSettled(userIds.map(id => getUser(id)))
    .then(outcomes => {
      return outcomes
        .filter(end result => end result.standing === 'fulfilled')
        .map(end result => end result.worth);
    });
}
Returning an error if the argument just isn’t a legitimate array

How to make use of Promise.race

Just like Promise.all or Promise.allSettled, the Promise.race static methodology takes an array of Promises, and returns a brand new Promise. As the title implies, although, it really works considerably in another way.

The Promise returned by Promise.race will wait till the primary of the given Promises is fulfilled or rejected, after which that Promise may also be fulfilled or rejected, with the identical worth. When this occurs, the fulfilled or rejected values of the opposite Promises are misplaced.

How to make use of Promise.any

Promise.any works equally to Promise.race with one key distinction – the place Promise.race shall be completed as quickly as any Promise is fulfilled or rejected, Promise.any waits for the primary fulfilled Promise.

How to Use async and await

async and await are particular key phrases that simplify working with Promises. They take away the necessity for callback features and calls to then or catch. They work with try-catch blocks, as nicely.

Here’s the way it works. Instead of calling then on a Promise, you await it by placing the await key phrase earlier than it. This successfully “pauses” execution of the operate till the Promise is fulfilled.

Here’s an instance utilizing commonplace Promises:

getUsers().then(customers => {
    console.log('Got customers:', customers);
});
Awaiting a promise with then

Here’s the equal code utilizing the await key phrase:

const customers = await getUsers();
console.log('Got customers:', customers);
Awaiting a promise with await

Promise chains are a little bit cleaner, too:

const knowledge = await readFile('sourceData.json');
const end result = await processData(knowledge);
await writeFile(end result, 'processedData.json');
Chaining guarantees with await

Remember that every utilization of await will pause execution of the remainder of the operate till the Promise you’re awaiting turns into fulfilled. If you need to await a number of Promises that run in parallel, you should use Promise.all:

const customers = await Promise.all([getUser(1), getUser(2), getUser(3)]);
Using Promise.all with await

To use the await key phrase, your operate have to be marked as an async operate. You can do that by putting the async key phrase earlier than your operate:

async operate processData(sourceFile, outputFile) {
  const knowledge = await readFile(sourceFile);
  const end result = await processData(knowledge);
  writeFile(end result, outputFile);
}
Marking a operate as async

Adding the async key phrase additionally has one other vital impact on the operate. Async features all the time implicitly return a Promise. If you come a price from an async operate, the operate will really return a Promise that’s fulfilled with that worth.

async operate add(a, b) {
  return a + b;   
}

add(2, 3).then(sum => {
   console.log('Sum is:', sum); 
});
An async operate so as to add two numbers

In the above instance, the operate is returning the sum of the 2 arguments a and b. But because it’s an async operate, it would not return the sum however slightly a Promise that’s fulfilled with the sum.

Error dealing with with async and await

We use await to attend for Promise to be fulfilled, however what about dealing with errors? If you’re awaiting a Promise, and it’s rejected, an error shall be thrown. This means to deal with the error, you’ll be able to put it in a try-catch block:

strive {
    const knowledge = await readFile(sourceFile);
    const end result = await processData(knowledge);
    await writeFile(end result, outputFile);
} catch (error) {
    console.error('Error occurred whereas processing:', error);
}
Error dealing with with a try-catch block

Promise Anti-Patterns

Unnecessarily creating a brand new Promise

Sometimes there is no getting round creating a brand new Promise. But if you’re already working with Promises returned by an API, you normally should not have to create your individual Promise:

operate getUsers() {
  return new Promise(resolve => {
     fetch('https://instance.com/api/customers')
       .then(end result => end result.json())
       .then(knowledge => resolve(knowledge))
  });
}
An instance of pointless Promise creation

In this instance, we’re creating a brand new Promise to wrap the Fetch API, which already returns Promises. This is pointless. Instead, simply return the Promise chain from the Fetch API instantly:

operate getUsers() {
  return fetch('https://instance.com/api/customers')
    .then(end result => end result.json());
}
Using the prevailing Fetch promise

In each instances, the code calling getUsers seems the identical:

getUsers()
  .then(customers => console.log('Got customers:', customers))
  .catch(error => console.error('Error fetching customers:', error));
   
Client code for both model of the getUsers operate

Swallowing errors

Consider this model of a getUsers operate:

operate getUsers() {
    return fetch('https://instance.com/api/customers')
    	.then(end result => end result.json())
    	.catch(error => console.error('Error loading customers:', error));
}
Swallowing the fetch error

Error dealing with is nice, proper? You may be stunned by the end result if we name this getUsers operate:

getUsers()
  .then(customers => console.log('Got customers:', customers))
  .catch(error => console.error('error:', error);)
Calling getUsers

You may anticipate this to print “error”, however it is going to really print “Got customers: undefined”. This is as a result of the catch name “swallows” the error and returns a brand new Promise that’s fulfilled with the return worth of the catch callback, which is undefined (console.error returns undefined). You’ll nonetheless see the “Error loading customers” log message from getUsers, however the returned Promise shall be fulfilled, not rejected.

If you need to catch the error contained in the getUsers operate and nonetheless reject the returned Promise, the catch handler must return a rejected Promise. You can do that by utilizing Promise.reject.

operate getUsers() {
  return fetch('https://instance.com/api/customers')
    .then(end result => end result.json())
    .catch(error => {
      console.error('Error loading customers:', error);
      return Promise.reject(error);
    });
}
Returning a rejected Promise after dealing with the error

Now you will nonetheless get the “Error loading customers” message, however the returned Promise may also be rejected with the error.

Nesting Promises

Avoid nesting Promise code. Instead, attempt to use flattened Promise chains.

Instead of this:

readFile(sourceFile)
  .then(knowledge => {
    processData(knowledge)
      .then(end result => {
        writeFile(end result, outputFile)
          .then(() => console.log('completed');
      });
  });

Do this:

readFile(sourceFile)
  .then(knowledge => processData(knowledge))
  .then(end result => writeFile(end result, outputFile))
  .then(() => console.log('completed'));

Summary

Here are the important thing factors for working with Promises:

  • A Promise may be pending, fulfilled, or rejected
  • A Promise is settled whether it is both fulfilled or rejected
  • Use then to get the fulfilled worth of a Promise
  • Use catch to deal with errors
  • Use lastly to carry out cleanup logic that you simply want in both the success or error case
  • Chain Promises collectively to carry out asynchronous duties in sequence
  • Use Promise.all to get a Promise that’s fulfilled when all given Promises are fulfilled, or rejects when a kind of Promises rejects
  • Use Promise.allSettled to get a Promise that’s fulfilled when all given Promises are both fulfilled or rejected
  • Use Promise.race to get a Promise that’s fulfilled or rejected when the primary of the given Promises is both fulfilled or rejected
  • Use Promise.any to get a Promise that’s fulfilled when the primary of the given Promises is fulfilled
  • Use the await key phrase to attend for the success worth of a Promise
  • Use a try-catch block to deal with errors when utilizing the await key phrase
  • A operate that makes use of await inside it should use the async key phrase

Thank you for studying this deep dive on Promises. I hope you realized one thing new!

You may also like

Leave a Comment