Mastering JavaScript: Dive Deep into Asynchronous JavaScript with Promises

Explore the power of asynchronous operations in JavaScript. Master Promises for efficient, non-blocking code and elevate your web development skills.

Mastering JavaScript: Dive Deep into Asynchronous JavaScript with Promises
Photo by Womanizer Toys / Unsplash

Imagine you're in a coffee shop. You place your order, and instead of standing there and waiting for your coffee, you decide to find a seat, read a book, or chat with a friend. When your coffee is ready, a barista will call your name, and you collect your drink. This is similar to how asynchronous execution works in JavaScript: you can kick off a task and continue doing other things without waiting for the initial task to complete.

JavaScript is single-threaded, which means it can only do one thing at a time. So, to avoid blocking the main thread (especially in web browsers), JavaScript uses asynchronous mechanisms. This is where Promises and other async techniques come into play.

Promises

A Promise in JavaScript represents a value that might not be available yet but will be at some point. A Promise can be in one of three states:

  1. Pending: The promise’s result hasn’t been determined yet.
  2. Fulfilled: The operation has completed successfully, and the promise has a resulting value.
  3. Rejected: An error occurred, and the promise won't be getting a value.

A typical Promise looks like this:

let promise = new Promise((resolve, reject) => {
    // Some asynchronous operation
    if (/* everything turned out fine */) {
        resolve("Result");
    } else {
        reject("Error");
    }
});

You can then handle the results with .then() and errors with .catch():

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

Promises in practice with practical examples

Certainly! Let's go through three practical scenarios where Promises come in handy.

1. Image Loading

Imagine you're building an image gallery, and you want to ensure an image has fully loaded before displaying it. You can create a function that returns a Promise to handle this.

function loadImage(url) {
    return new Promise((resolve, reject) => {
        const img = new Image();
        img.src = url;
        
        img.onload = () => {
            resolve(img);
        };
        
        img.onerror = () => {
            reject(new Error(`Failed to load image at ${url}`));
        };
    });
}

loadImage('path/to/image.jpg')
    .then(img => {
        document.body.appendChild(img);
    })
    .catch(error => {
        console.error(error);
    });

Let's break this down step by step.

Code Explanation

Function Definition: loadImage(url)

The main purpose of loadImage() function is to load an image from a given URL and notify us whether the loading was successful or if it filed with an error.

Returning a Promise:

   return new Promise((resolve, reject) => {

Here, we're returning a new Promise from the function which will give back a value now, later, or never

The Promise takes two functions as arguments: resolve and reject.

  • resolve: This function will get executed when the desired operation completes successfully.
  • reject: This function will be called if the operation failed with an error.
  1. Creating a New Image:
const img = new Image();
img.src = url;

This code is  creating a new image object and setting its src attribute to the provided URL. This will trigger the process of loading the image from that URL.

  1. Handling the Image Loading:
img.onload = () => {
   resolve(img);
};

The onload event is set to be a function that gets called once the image is fully loaded. When this happens, we call the resolve function of the promise, indicating that the image loading was successful, and we pass the loaded image object (img) as the value.

  1. Handling Image Load Errors:
img.onerror = () => {
   reject(new Error(`Failed to load image at ${url}`));
};

A function is assigned to onerror event of the image object, this will get executed if there's an error while trying to load the image.

  1. Using the loadImage Function:
loadImage('path/to/image.jpg')

Here, we're actually calling the loadImage function to load an image from given URL as an argument..

  1. Handling Successful Image Loading:
.then(img => {
   document.body.appendChild(img);
})

The code inside the .then()  body will execute, if the load image operation was sucessfull,  resulting in appending the loaded image to document's body.

  1. Handling Image Load Errors:
.catch(error => {
   console.error(error);
});

In case of an error, the catch() part will execute, and as a result the error will printed to console.

2. Delay Execution

Perhaps you want to delay the execution of a function by a certain amount of time. This could be useful in animations, user feedback, or any situation where a time delay is required.

function delay(milliseconds) {
    return new Promise(resolve => {
        setTimeout(resolve, milliseconds);
    });
}

delay(2000)
    .then(() => {
        console.log('This will be logged after 2 seconds!');
    });

3. Reading a File (Node.js)

Following example is in context of Node.js, where we often need to work with the filesystem. Following example shows, how can use Promise instead of callback to read a file in asynchronous manner.

const fs = require('fs');

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

readFilePromise('./path/to/file.txt')
    .then(content => {
        console.log(content);
    })
    .catch(error => {
        console.error(`Error reading file: ${error.message}`);
    });

Using Promise to call REST APIs in JavaScript (the fetch() function)

A practical application of Promises in modern web development is the fetch() function. It's used to make network requests and returns a Promise.

Here's an example using a TODO rest API, provided by jsonplaceholder.typicode.com which is a free fake online REST API for testing:

fetch('https://jsonplaceholder.typicode.com/todos/1')
    .then(response => {
        if (!response.ok) {
            throw new Error(`HTTP error! Status: ${response.status}`);
        }
        return response.json(); // This also returns a Promise!
    })
    .then(data => {
        console.log(data);
    })
    .catch(error => {
        console.error('Fetch error: ' + error.message);
    });

In this example:

  1. We make a request to get a to-do item with ID 1.
  2. Once the request resolves, we check if the response was okay. If not, we throw an error.
  3. We then parse the JSON response. Note: response.json() itself returns a Promise because reading the stream to completion is asynchronous.
  4. Finally, we log the resulting data or catch any errors.

In essence, Promises and asynchronous execution allow you to write non-blocking code in a more readable and maintainable manner, especially in situations where you need to work with external data or operations that might take some time. Just like not waiting idly for your coffee, your code can move on and handle other tasks, fetching the results when they're ready!

🌟 Join the Conversation! 🌟
Your thoughts and perspectives enrich our community. Dive in, share your insights, and be a part of the dynamic exchange. Drop a comment below—we can't wait to hear from you! 👇📝🔥