Post

Introduction to Promises in JavaScript

JavaScript is a powerful and popular programming language that runs on browsers, servers, and many other platforms. However, JavaScript also has some challenges when it comes to dealing with asynchronous code, which is code that does not execute in a sequential order. In this blog post, we will learn about Promises, which are a way of handling asynchronous code in JavaScript. We will cover the following topics:

  • What are synchronous and asynchronous code and why they matter in JavaScript
  • What are Promises and how they work
  • How to chain multiple Promises together
  • How to create your own Promises
  • How to use Promises in real-world scenarios
  • How to combine multiple Promises with Promise.all() and Promise.race()
  • How to handle errors in Promises
  • What are some best practices for using Promises

By the end of this blog post, you will have a solid understanding of Promises and how they can make your JavaScript code more readable, reliable, and efficient.

Synchronous vs. Asynchronous Code

Before we dive into Promises, let’s first understand what synchronous and asynchronous code mean and why they are important in JavaScript.

Synchronous code is code that executes in a sequential order, meaning that each line of code waits for the previous line to finish before running. For example, consider the following code snippet:

1
2
console.log("Hello");
console.log("World");

This code is synchronous because it prints “Hello” first and then “World” second. The second line of code waits for the first line to finish before running.

Asynchronous code is code that does not execute in a sequential order, meaning that some lines of code can run without waiting for the previous lines to finish. For example, consider the following code snippet:

1
2
console.log("Hello");
setTimeout(() => console.log("World"), 1000);

This code is asynchronous because it prints “Hello” first and then “World” after one second. The second line of code does not wait for the first line to finish before running. Instead, it sets up a timer that will run the callback function after one second.

Asynchronous code is very common in JavaScript because it allows us to perform tasks that take some time without blocking the rest of the code. For example, we can use asynchronous code to fetch data from a remote server, read or write files, set timers, or interact with user events.

However, asynchronous code also has some challenges in JavaScript. One of the main challenges is that it can be difficult to write and read asynchronous code that depends on the results of other asynchronous operations. For example, consider the following code snippet:

1
2
3
4
5
6
7
8
9
10
11
12
// fetch data from a remote server
fetch('https://example.com/data.json')
  .then(data => data.json()) // parse the data as JSON
  .then(json => {
    // print the JSON data
    console.log(json);
    // fetch more data from another server
    return fetch('https://example.com/another-data.json');
  })
  .then(data => data.json()) // parse the second data as JSON
  .then(json => console.log(json)) // print the second JSON data
  .catch(error => console.error(error)); // handle any errors

This code is using a feature called fetch, which is a built-in function that returns a Promise (I will explain what a Promise is soon). The fetch function takes a URL as an argument and returns a Promise that resolves with the response from the server. We can use the .then() method on the Promise to register a callback function that will run when the Promise is resolved. We can also use the .catch() method on the Promise to register a callback function that will run when the Promise is rejected (meaning that something went wrong).

The problem with this code is that it can be hard to follow and understand. We have to nest multiple .then() callbacks inside each other, creating a “Pyramid of Doom” or “Callback Hell”. This makes our code less readable and more prone to errors.

This is where Promises come in handy. Promises are a way of writing and handling asynchronous code in JavaScript that makes it easier to avoid nesting callbacks and handle errors.

Understanding Promises

A Promise is an object that represents an asynchronous operation that will either succeed or fail in the future. A Promise has three possible states:

  • Pending: The Promise is not yet resolved or rejected. This means that the asynchronous operation is still in progress.
  • Fulfilled: The Promise is resolved with a value. This means that the asynchronous operation was successful and produced a result.
  • Rejected: The Promise is rejected with a reason. This means that the asynchronous operation failed and produced an error.

The basic syntax of creating a Promise object is as follows:

1
2
3
4
5
let promise = new Promise((resolve, reject) => {
  // some asynchronous code
  // call resolve(value) if successful
  // call reject(reason) if failed
});

The Promise constructor takes a function as an argument, which is called the executor. The executor function receives two parameters: resolve and reject, which are functions that can be used to resolve or reject the Promise. The executor function runs immediately when the Promise is created and contains the asynchronous code that will produce a value or an error.

To get the value or the error from a Promise, we can use the .then() and .catch() methods on the Promise object. The .then() method takes two optional arguments: a callback function that will run if the Promise is fulfilled, and another callback function that will run if the Promise is rejected. The .catch() method takes one argument: a callback function that will run if the Promise is rejected. The .then() and .catch() methods return another Promise, which allows us to chain multiple Promises together.

Here is an example of how to use a Promise:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
let promise = new Promise((resolve, reject) => {
  // simulate an asynchronous operation that takes 1 second
  setTimeout(() => {
    // generate a random number between 0 and 1
    let randomNumber = Math.random();
    // if the number is greater than 0.5, resolve the promise with the number
    if (randomNumber > 0.5) {
      resolve(randomNumber);
    }
    // otherwise, reject the promise with an error message
    else {
      reject("The number is too low");
    }
  }, 1000);
});

// register a callback function that will run if the promise is fulfilled
promise.then(value => {
  console.log("The promise was resolved with value:", value);
});

// register a callback function that will run if the promise is rejected
promise.catch(error => {
  console.error("The promise was rejected with reason:", error);
});

This code creates a Promise that simulates an asynchronous operation that takes one second and resolves or rejects with a random number or an error message. Then, it registers two callback functions that will print the value or the error to the console.

If we run this code multiple times, we will see different outputs depending on the random number generated. For example, we might see:

1
The promise was resolved with value: 0.789456123

or

1
The promise was rejected with reason: The number is too low

This is how we can use Promises to handle asynchronous code in JavaScript.

Chaining Promises

One of the main benefits of Promises is that they allow us to chain multiple asynchronous operations together without nesting callbacks. This makes our code more readable and maintainable.

To chain Promises, we can use the .then() method on a Promise and return another Promise from the callback function. This way, the next .then() method in the chain will wait for the previous Promise to be resolved before running its callback function. We can also use the .catch() method at the end of the chain to handle any errors that might occur in any of the Promises.

Here is an example of how to chain Promises:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// create a function that returns a promise that resolves after n milliseconds
function wait(n) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(`Waited for ${n} milliseconds`);
    }, n);
  });
}

// create a function that returns a promise that rejects after n milliseconds
function fail(n) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject(`Failed after ${n} milliseconds`);
    }, n);
  });
}

// chain multiple promises together
wait(1000) // wait for 1 second
  .then(value => {
    console.log(value); // print "Waited for 1000 milliseconds"
    return wait(2000); // return another promise that waits for 2 seconds
  })
  .then(value => {
    console.log(value); // print "Waited for 2000 milliseconds"
    return fail(3000); // return another promise that fails after 3 seconds
  })
  .then(value => {
    console.log(value); // this will not run because the previous promise was rejected
    return wait(4000); // this will not run either because the chain was broken by an error
  })
  .catch(error => {
    console.error(error); // print "Failed after 3000 milliseconds"
  });

This code creates two functions that return Promises that either resolve or reject after some time. Then, it chains multiple Promises together using the .then() method and handles any errors using the .catch() method.

If we run this code, we will see the following output:

1
2
3
Waited for 1000 milliseconds
Waited for 2000 milliseconds
Failed after 3000 milliseconds

This shows how we can chain multiple asynchronous operations together using Promises and handle any errors that might occur along the way.

Creating Promises

Sometimes, we might want to create our own Promises instead of using the built-in ones. This can be useful when we want to wrap some existing asynchronous code in a Promise or when we want to create a custom Promise for some specific purpose.

To create a Promise manually, we can use the Promise constructor as we saw before. The Promise constructor takes a function as an argument, which is called the executor. The executor function receives two parameters: resolve and reject, which are functions that can be used to resolve or reject the Promise. The executor function runs immediately when the Promise is created and contains the asynchronous code that will produce a value or an error.

Here is an example of how to create a Promise manually:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// create a function that returns a promise that resolves with a random number
function getRandomNumber() {
  return new Promise((resolve, reject) => {
    // simulate an asynchronous operation that takes 1 second
    setTimeout(() => {
      // generate a random number between 0 and 100
      let randomNumber = Math.floor(Math.random() * 100);
      // resolve the promise with the random number
      resolve(randomNumber);
    }, 1000);
  });
}

// use the custom promise function
getRandomNumber()
  .then(number => {
    console.log("The random number is:", number);
  })
  .catch(error => {
    console.error("Something went wrong:", error);
  });

This code creates a function that returns a Promise that resolves with a random number after one second. Then, it uses the .then() and .catch() methods on the returned Promise to handle the value or the error.

If we run this code, we will see something like:

1
The random number is: 42

This is how we can create our own Promises manually.

However, creating Promises manually can be tedious and error-prone. Fortunately, JavaScript provides some built-in constructors that can help us create Promises more easily. Some of these constructors are:

  • Promise.resolve(value): This constructor returns a Promise that is already resolved with the given value. This can be useful when we want to create a Promise that does not involve any asynchronous code or when we want to convert a non-Promise value into a Promise.
  • Promise.reject(reason): This constructor returns a Promise that is already rejected with the given reason. This can be useful when we want to create a Promise that always fails or when we want to simulate an error for testing purposes.
  • Promise.all(iterable): This constructor takes an iterable (such as an array) of Promises and returns a Promise that resolves with an array of the values of the resolved Promises, or rejects with the reason of the first rejected Promise. This can be useful when we want to wait for multiple Promises to finish and get their results together.
  • Promise.race(iterable): This constructor takes an iterable (such as an array) of Promises and returns a Promise that resolves or rejects with the value or the reason of the first Promise that settles (either resolves or rejects). This can be useful when we want to handle the fastest Promise among multiple Promises and ignore the rest.

Here are some examples of how to use these built-in constructors:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// create a promise that is already resolved with "Hello"
let promise1 = Promise.resolve("Hello");

// create a promise that is already rejected with "Goodbye"
let promise2 = Promise.reject("Goodbye");

// create a promise that waits for 1 second and resolves with "World"
let promise3 = wait(1000);

// use Promise.all to wait for all promises and get their values
Promise.all([promise1, promise2, promise3])
  .then(values => {
    console.log(values); // this will not run because one of the promises was rejected
  })
  .catch(error => {
    console.error(error); // this will print "Goodbye"
  });

// use Promise.race to handle the fastest promise and ignore the rest
Promise.race([promise1, promise2, promise3])
  .then(value => {
    console.log(value); // this will print "Hello"
  })
  .catch(error => {
    console.error(error); // this will not run because the fastest promise was resolved
  });

This code creates three Promises using different constructors and then uses Promise.all and Promise.race to handle them.

If we run this code, we will see the following output:

1
2
Hello
Goodbye

This shows how we can use the built-in constructors to create Promises more easily.

Some common use cases for creating Promises are:

  • Wrapping existing asynchronous code in a Promise. For example, we can wrap the setTimeout function in a Promise to create a timer that can be used with .then() and .catch().
  • Creating custom Promises for specific purposes. For example, we can create a Promise that resolves with a user input or rejects with a timeout.
  • Converting non-Promise values into Promises. For example, we can convert a plain value or an array into a Promise using Promise.resolve so that we can use it with other Promises.

Promises in Real-World Scenarios

Fetching data from a remote server with Promises

One of the most common use cases for Promises is fetching data from a remote server. For example, we might want to get some JSON data from an API endpoint and display it on our web page.

To fetch data from a remote server, we can use the built-in fetch function, which returns a Promise that resolves with the response from the server. The response object has a .json() method that returns another Promise that resolves with the parsed JSON data. We can use the .then() and .catch() methods on these Promises to handle the data or the errors.

Here is an example of how to fetch data from a remote server with Promises:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// fetch data from a fake API endpoint
fetch('https://jsonplaceholder.typicode.com/todos/1')
  .then(response => {
    // check if the response is ok
    if (response.ok) {
      // return a promise that resolves with the parsed JSON data
      return response.json();
    }
    // otherwise, throw an error
    else {
      throw new Error(`Something went wrong: ${response.status}`);
    }
  })
  .then(data => {
    // print the JSON data to the console
    console.log(data);
    // display the JSON data on the web page
    document.getElementById('data').innerHTML = JSON.stringify(data, null, 2);
  })
  .catch(error => {
    // print the error to the console
    console.error(error);
    // display the error message on the web page
    document.getElementById('data').innerHTML = error.message;
  });

This code fetches data from a fake API endpoint that returns a JSON object with some information about a todo item. Then, it checks if the response is ok and parses the JSON data. Finally, it prints and displays the data or the error on the web page.

If we run this code, we will see something like:

1
2
3
4
5
6
{
  "userId": 1,
  "id": 1,
  "title": "delectus aut autem",
  "completed": false
}

This shows how we can fetch data from a remote server with Promises.

Working with timers and Promises

Another common use case for Promises is working with timers. For example, we might want to create a countdown timer that updates every second and stops when it reaches zero.

To work with timers and Promises, we can use the setTimeout function, which takes a callback function and a delay in milliseconds as arguments and executes the callback function after the delay. We can wrap the setTimeout function in a Promise to create a timer that can be used with .then() and .catch().

Here is an example of how to work with timers and Promises:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// create a function that returns a promise that resolves after n milliseconds
function wait(n) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(`Waited for ${n} milliseconds`);
    }, n);
  });
}

// create a function that creates a countdown timer using promises
function countdown(seconds) {
  // create an array of promises that resolve every second
  let promises = [];
  for (let i = seconds; i > 0; i--) {
    promises.push(wait((seconds - i) * 1000));
  }
  // use Promise.all to wait for all promises and get their values
  Promise.all(promises)
    .then(values => {
      // print and display the values on the web page
      values.forEach(value => {
        console.log(value);
        document.getElementById('timer').innerHTML += value + '<br>';
      });
      // print and display "Time's up!" when the countdown is over
      console.log("Time's up!");
      document.getElementById('timer').innerHTML += "Time's up!";
    })
    .catch(error => {
      // print and display any errors
      console.error(error);
      document.getElementById('timer').innerHTML = error.message;
    });
}

// create a countdown timer for 5 seconds
countdown(5);

This code creates a function that returns a Promise that resolves after some time. Then, it creates another function that creates an array of Promises that resolve every second. Finally, it uses Promise.all to wait for all Promises and print and display their values on the web page.

If we run this code, we will see something like:

1
2
3
4
5
6
Waited for 0 milliseconds
Waited for 1000 milliseconds
Waited for 2000 milliseconds
Waited for 3000 milliseconds
Waited for 4000 milliseconds
Time's up!

This shows how we can work with timers and Promises.

File handling and Promises

Another common use case for Promises is file handling. For example, we might want to read or write files on our local machine or on a remote server.

To handle files and Promises, we can use the built-in FileReader and FileWriter objects, which provide methods for reading and writing files. These methods return Promises that resolve with the file data or reject with an error. We can use the .then() and .catch() methods on these Promises to handle the file data or the errors.

Here is an example of how to handle files and Promises:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// create a function that returns a promise that reads a file
function readFile(file) {
  return new Promise((resolve, reject) => {
    // create a FileReader object
    let reader = new FileReader();
    // register a callback function that will run when the file is read
    reader.onload = () => {
      // resolve the promise with the file data
      resolve(reader.result);
    };
    // register a callback function that will run when an error occurs
    reader.onerror = () => {
      // reject the promise with the error
      reject(reader.error);
    };
    // read the file as text
    reader.readAsText(file);
  });
}

// create a function that returns a promise that writes a file
function writeFile(file, data) {
  return new Promise((resolve, reject) => {
    // create a FileWriter object
    let writer = new FileWriter(file);
    // register a callback function that will run when the file is written
    writer.onwriteend = () => {
      // resolve the promise with the file
      resolve(file);
    };
    // register a callback function that will run when an error occurs
    writer.onerror = () => {
      // reject the promise with the error
      reject(writer.error);
    };
    // write the data to the file
    writer.write(data);
  });
}

// get a file input element from the web page
let fileInput = document.getElementById('file-input');

// register a callback function that will run when a file is selected
fileInput.onchange = () => {
  // get the selected file from the input element
  let file = fileInput.files[0];
  // read the file using promises
  readFile(file)
    .then(data => {
      // print and display the file data on the web page
      console.log(data);
      document.getElementById('file-data').innerHTML = data;
      // modify the file data by adding some text
      let newData = data + '\nThis is some added text.';
      // write the modified data to a new file using promises
      return writeFile(new File([newData], 'new-file.txt'));
    })
    .then(newFile => {
      // print and display the new file name on the web page
      console.log(newFile.name);
      document.getElementById('file-name').innerHTML = newFile.name;
    })
    .catch(error => {
      // print and display any errors on the web page
      console.error(error);
      document.getElementById('file-error').innerHTML = error.message;
    });
};

This code creates two functions that return Promises that read and write files. Then, it gets a file input element from the web page and registers a callback function that will run when a file is selected. Finally, it reads, modifies, and writes the file using Promises and prints and displays the results on the web page.

If we run this code and select a text file, we will see something like:

1
2
3
Hello, this is a text file.
This is some added text.
new-file.txt

This shows how we can handle files and Promises.

Promise.all() and Promise.race()

As we have seen before, Promise.all() and Promise.race() are two built-in constructors that can help us combine multiple Promises into one. Promise.all() takes an iterable of Promises and returns a Promise that resolves with an array of the values of the resolved Promises, or rejects with the reason of the first rejected Promise. Promise.race() takes an iterable of Promises and returns a Promise that resolves or rejects with the value or the reason of the first Promise that settles.

These constructors can be useful when we want to handle multiple Promises in different ways. For example, we might want to wait for all Promises to finish and get their results together, or we might want to handle the fastest Promise among multiple Promises and ignore the rest.

Here are some examples of how to use Promise.all() and Promise.race():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// create some promises that resolve or reject after some time
let promiseA = wait(1000); // resolves after 1 second
let promiseB = wait(2000); // resolves after 2 seconds
let promiseC = fail(3000); // rejects after 3 seconds

// use Promise.all to wait for all promises and get their values
Promise.all([promiseA, promiseB, promiseC])
  .then(values => {
    console.log(values); // this will not run because one of the promises was rejected
  })
  .catch(error => {
    console.error(error); // this will print "Failed after 3000 milliseconds"
  });

// use Promise.race to handle the fastest promise and ignore the rest
Promise.race([promiseA, promiseB, promiseC])
  .then(value => {
    console.log(value); // this will print "Waited for 1000 milliseconds"
  })
  .catch(error => {
    console.error(error); // this will not run because the fastest promise was resolved
  });

This code creates some Promises that resolve or reject after some time. Then, it uses Promise.all and Promise.race to handle them.

If we run this code, we will see something like:

1
2
Waited for 1000 milliseconds
Failed after 3000 milliseconds

This shows how we can use Promise.all and Promise.race to combine multiple Promises.

Some practical examples of using Promise.all and Promise.race are:

  • Using Promise.all to perform multiple HTTP requests in parallel and get their results together. For example, we can use Promise.all to fetch data from multiple API endpoints and display them on the web page.
  • Using Promise.race to implement a timeout for a Promise. For example, we can use Promise.race to reject a Promise if it takes too long to resolve and display an error message on the web page.

The role of try...catch with Promises

Another way of dealing with errors in Promises is using the try...catch statement, which is a built-in feature of JavaScript that allows us to execute some code and catch any errors that might occur. The try...catch statement has two blocks: a try block that contains the code that might throw an error, and a catch block that contains the code that will handle the error.

To use the try...catch statement with Promises, we can use the async/await syntax, which is a modern feature of JavaScript that allows us to write asynchronous code in a synchronous way. The async/await syntax consists of two keywords: async and await. The async keyword is used to declare an asynchronous function that returns a Promise. The await keyword is used to pause the execution of the function until the Promise is resolved or rejected.

Here is an example of how to use the try...catch statement with Promises:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// create an async function that uses try...catch to handle errors
async function handleErrors() {
  try {
    // await for a promise that rejects with an error
    let value = await fail(1000);
    // this will not run because the promise was rejected
    console.log(value);
  } catch (error) {
    // print and display the error on the web page
    console.error(error); // this will print "Failed after 1000 milliseconds"
  }
}

// call the async function
handleErrors();

This code creates an async function that uses the try...catch statement to handle errors. Then, it calls the function and awaits for a Promise that rejects with an error. Finally, it prints and displays the error on the web page.

If we run this code, we will see something like:

1
Failed after 1000 milliseconds

This shows how we can use the try...catch statement with Promises.

Best Practices for Using Promises

Promises are a powerful and elegant way of writing and handling asynchronous code in JavaScript, but they also require some care and attention to avoid common pitfalls and mistakes. In this section, we will look at some best practices for using Promises that can help us write better and cleaner code.

Avoiding the “Pyramid of Doom” with Promises

One of the main advantages of Promises is that they allow us to avoid nesting callbacks, which can create a “Pyramid of Doom” or “Callback Hell”. This is a situation where we have multiple levels of indentation and callbacks inside callbacks, making our code hard to read and maintain.

To avoid the “Pyramid of Doom” with Promises, we should use the .then() method to chain multiple Promises together instead of nesting them inside each other. This way, we can keep our code flat and linear, making it easier to follow and understand.

Here is an example of how to avoid the “Pyramid of Doom” with Promises:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// resolves or rejects randomly based on number generated
function randomPromise() {
    return new Promise((resolve, reject) => {
        // generate a random number between 0 and 1
        let randomNumber = Math.random();
        // if the number is greater than 0.5, resolve the promise with the number
        if (randomNumber > 0.5) {
          resolve(randomNumber);
        }
        // otherwise, reject the promise with an error message
        else {
          reject("The number is too low");
        }
    });
}

// create some promises that resolve or reject randomly
let promiseA = randomPromise(); // resolves or rejects randomly
let promiseB = randomPromise(); // resolves or rejects randomly
let promiseC = randomPromise(); // resolves or rejects randomly

// chain multiple promises together using .then()
promiseA // start with promiseA
  .then(value => {
    console.log(value); // print the value if resolved
    return promiseB; // return promiseB
  })
  .then(value => {
    console.log(value); // print the value if resolved
    return promiseC; // return promiseC
  })
  .then(value => {
    console.log(value); // print the value if resolved
  })
  .catch(error => {
    console.error(error); // print the error if rejected
  });

This code creates some Promises that resolve or reject randomly. Then, it chains them together using the .then() method instead of nesting them inside each other.

If we run this code, we will see something like:

1
2
The number is too low
The number is too low

or

1
2
0.789456123
The number is too low

or

1
2
3
0.789456123
0.654321789
0.123456789

This shows how we can avoid the “Pyramid of Doom” with Promises.

Proper error handling

Another important aspect of using Promises is proper error handling. Error handling is the process of detecting, catching, and handling errors that might occur in our code. Error handling can help us prevent our program from crashing, leaking resources, or producing incorrect results.

To handle errors properly with Promises, we should follow these guidelines:

  • Use the .catch() method at the end of every Promise chain to catch and handle any errors that might occur in any of the Promises in the chain.
  • Use the Promise.reject() constructor to create and return a Promise that is already rejected with an error object. This can help us propagate errors to the next .catch() method in the chain.
  • Use the throw statement to throw an error object from a .then() callback function. This can help us reject the Promise returned by the .then() method and propagate the error to the next .catch() method in the chain.
  • Use the try...catch statement with the async/await syntax to handle errors in an async function. This can help us write synchronous-looking code that can handle errors in a familiar way.

Here is an example of how to handle errors properly with Promises:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// create a function that returns a promise that validates a user input
function validateInput(input) {
  return new Promise((resolve, reject) => {
    // check if the input is a valid email address
    let regex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/;
    if (regex.test(input)) {
      // resolve the promise with the input
      resolve(input);
    } else {
      // reject the promise with an error object
      reject(new Error("Invalid email address"));
    }
  });
}

// create an async function that uses try...catch to handle errors
async function handleInput() {
  try {
    // get the user input from the web page
    let input = document.getElementById('input').value;
    // await for the validation promise
    let email = await validateInput(input);
    // print and display the email on the web page
    console.log(email);
    document.getElementById('email').innerHTML = email;
  } catch (error) {
    // print and display the error on the web page
    console.error(error);
    document.getElementById('error').innerHTML = error.message;
  }
}

// call the async function
handleInput();

This code creates a function that returns a Promise that validates a user input as an email address. Then, it creates an async function that uses the try...catch statement to handle errors. Finally, it calls the async function and awaits for the validation Promise.

If we run this code and enter a valid email address, we will see something like:

1
john.doe@example.com

If we run this code and enter an invalid email address, we will see something like:

1
Error: Invalid email address

This shows how we can handle errors properly with Promises.

Using async/await with Promises

As we have seen before, async/await is a modern feature of JavaScript that allows us to write asynchronous code in a synchronous way. The async/await syntax consists of two keywords: async and await. The async keyword is used to declare an asynchronous function that returns a Promise. The await keyword is used to pause the execution of the function until the Promise is resolved or rejected.

Using async/await with Promises can help us write cleaner and simpler code that avoids nesting callbacks and chaining .then() methods. It can also help us handle errors using the try...catch statement, which is more familiar and intuitive than using .catch() methods.

Here is an example of how to use async/await with Promises:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// resolves or rejects randomly based on number generated
function randomPromise() {
    return new Promise((resolve, reject) => {
        // generate a random number between 0 and 1
        let randomNumber = Math.random();
        // if the number is greater than 0.5, resolve the promise with the number
        if (randomNumber > 0.5) {
          resolve(randomNumber);
        }
        // otherwise, reject the promise with an error message
        else {
          reject("The number is too low");
        }
    });
}

// create some promises that resolve or reject randomly
let promiseA = randomPromise(); // resolves or rejects randomly
let promiseB = randomPromise(); // resolves or rejects randomly
let promiseC = randomPromise(); // resolves or rejects randomly

// create an async function that uses await to handle promises
async function handlePromises() {
  try {
    // await for promiseA and print its value if resolved
    let valueA = await promiseA;
    console.log(valueA);
    // await for promiseB and print its value if resolved
    let valueB = await promiseB;
    console.log(valueB);
    // await for promiseC and print its value if resolved
    let valueC = await promiseC;
    console.log(valueC);
  } catch (error) {
    // print any errors if rejected
    console.error(error);
  }
}

// call the async function
handlePromises();

This code creates some Promises that resolve or reject randomly. Then, it creates an async function that uses await to handle them. Finally, it calls the async function and awaits for each Promise.

If we run this code, we will see something like:

1
2
The number is too low
The number is too low

or

1
2
0.789456123
The number is too low

or

1
2
3
0.789456123
0.654321789
0.123456789

This shows how we can use async/await with Promises.

Conclusion

In this blog post, we have learned about Promises, which are a way of handling asynchronous code in JavaScript. We have covered the following topics:

  • What are synchronous and asynchronous code and why they matter in JavaScript
  • What are Promises and how they work
  • How to chain multiple Promises together
  • How to create your own Promises
  • How to use Promises in real-world scenarios
  • How to combine multiple Promises with Promise.all() and Promise.race()
  • How to handle errors in Promises
  • What are some best practices for using Promises

Promises are a powerful and elegant way of writing and handling asynchronous code in JavaScript, but they also require some care and attention to avoid common pitfalls and mistakes. By following the guidelines and examples in this blog post, you can write better and cleaner code that uses Promises effectively.

Promises are also a significant feature of modern JavaScript, as they are used by many libraries and frameworks that rely on asynchronous code. By learning how to use Promises, we can also learn how to use these libraries and frameworks more easily and efficiently.

I hope that this blog post has ed you understand and appreciate Promises in JavaScript. I encourage you to further explore and learn more about Promises and how they can improve your JavaScript skills and projects.

Happy coding!

This post is licensed under CC BY 4.0 by the author.