Simplifying JavaScript Promises: A Kid-Friendly Guide

Sanjeev SinghSanjeev Singh
9 min read

If you're learning JavaScript, understanding this concept is essential. Promises are used for asynchronous programming. So, first, let's understand what asynchronous programming is and why we need promises for it.

What is Asynchronous Programming?

In web development, if you are trying to load something from a different place, like a server, you need to send some information to the server and then wait for a response. This communication takes time. You can either wait idly for the response, or you can continue working on other tasks while the server processes your request. This is where promises come in.

Promises allow you to handle tasks that are loading on your website or webpage asynchronously. If something is loading directly and you wait for it, that's synchronous. However, if you request something and continue with other tasks while waiting for the response, that's asynchronous. Think of it like ordering food at a restaurant: you place your order and then do other things while waiting for your meal. When your order is ready, you can start eating. This example illustrates how asynchronous programming works.

Now let's move on to the main topic:

What are promises in JavaScript?

A promise is an object that accepts a function as an argument and then returns either a resolved or rejected state. It has three states. When you make an API request to a server, the promise can be in one of these states:

  1. Pending: The request has been sent to the server, but the response has not yet been received.

  2. Fulfilled: The server has successfully processed the request, and the response has been received.

  3. Rejected: The server encountered an error while processing the request, and the response indicates a failure.

This is how promises work in JavaScript.

Promise Syntax:

let promise = new Promise(function(resolve, reject) {
  // asynchronous operation
  if (success) {
    resolve(value); // success
  } else {
    reject(error); // failure
  }
});

Let's understand this code line by line,

let promise = new Promise(function(resolve, reject) {

Here we are creating a Promise and assigning this to a variable named 'promise'. This variable will hold any value that comes after performing the Promise operations.

function(resolve, reject){
  // asynchronous operation
}

function(resolve, reject): This is the asynchronous function that takes 2 arguments(resolve and reject). These arguments are not simple arguments just like we accept in a normal function instead these(resolve, reject) are the functions provided by Promises Constructor itself.

// asynchronous operation

// asynchronous operation: Instructions are used to indicate that here is the place where you can write your code to perform an Asynchronous operation like (e.g., a network request(API), file read, timer, etc.).

if (success) {
}

if (success): This condition checks if the asynchronous operation was successful. success is a placeholder for a real condition that determines the success of the operation.

resolve(value); // success

resolve(value);: If the operation was successful, the resolve function is called with value as an argument. This changes the state of the promise to “fulfilled” and passes the value to the next then handler.

  } else {
    reject(error); // failure

else { reject(error); }: If the operation was not successful, the reject function is called with error as an argument. This changes the state of the promise to “rejected” and passes the error to the catch handler.

Now let's take the example and understand how it works.

Promises Example:

let promise = new Promise(function(resolve, reject) {
  // Simulate an asynchronous operation with a timeout
  setTimeout(() => {
    let success = true; // Placeholder condition
    if (success) {
      resolve("Operation was successful!"); // Fulfill the promise
    } else {
      reject("Operation failed!"); // Reject the promise
    }
  }, 1000); // Wait 1 second before deciding success or failure
});

// Handling the promise
promise
  .then((message) => {
    console.log(message); // Logs: "Operation was successful!"
  })
  .catch((error) => {
    console.error(error); // Logs: "Operation failed!" if promise is rejected
  });

Here are two things happening:

let promise = new Promise(function(resolve, reject) {
  // Simulate an asynchronous operation with a timeout
  setTimeout(() => {
    let success = true; // Placeholder condition
    if (success) {
      resolve("Operation was successful!"); // Fulfill the promise
    } else {
      reject("Operation failed!"); // Reject the promise
    }
  }, 1000); // Wait 1 second before deciding success or failure
});

In this part of the code, a promise is created, and to simulate an asynchronous operation, we make it wait for 1 second using setTimeout().

The first section is for creating the promise, and the second section is for handling it, whether it is fulfilled or rejected.

// Handling the promise
promise
  .then((message) => {
    console.log(message); // Logs: "Operation was successful!"
  })
  .catch((error) => {
    console.error(error); // Logs: "Operation failed!" if promise is rejected
  });

1. promise.then((message) => { ... }).catch((error) => { ... });:

• This block handles the promise when it is either fulfilled or rejected.

2. Handling fulfillment:

.then((message) => { console.log(message); }):

  • This attaches a callback function that runs when the promise is fulfilled.

  • The fulfilled value ("Operation was successful!") is passed as a message to this callback.

  • The message is then logged to the console.

3. Handling rejection:

.catch((error) => { console.error(error); }):

  • This attaches a callback function that runs when the promise is rejected.

  • The reason for the rejection ("Operation failed!") is passed as an error to this callback.

  • The error message is then logged to the console.

Another Complex example using APIs and fetch in Promises

function fetchUserData(userId) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (userId) {
                resolve({ userId: userId, username: 'john_doe' });
            } else {
                reject('User ID is missing');
            }
        }, 1000);
    });
}

function fetchUserPosts(username) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (username) {
                resolve(['Post 1', 'Post 2', 'Post 3']);
            } else {
                reject('Username is missing');
            }
        }, 1000);
    });
}

const userId = 1;

fetchUserData(userId)
    .then((userData) => {
        console.log('User Data:', userData);
        return fetchUserPosts(userData.username);
    })
    .then((userPosts) => {
        console.log('User Posts:', userPosts);
    })
    .catch((error) => {
        console.error('Error:', error);
    });

Interview Questions Around Promises:

  1. Explain how the fetch API works and how it returns a promise.

The fetch API in JavaScript is used to make HTTP requests to servers. It is a modern alternative to XMLHttpRequest and returns a promise. When you call fetch(url), it returns a promise that resolves to the response of the request, which contains information like status, headers, and body.

fetch('https://api.example.com/data')
  .then(response => {
    if (!response.ok) {
      throw new Error('Network response was not ok');
    }
    return response.json(); // Converts the response body to JSON
  })
  .then(data => {
    console.log(data); // Process the data
  })
  .catch(error => {
    console.error('There was a problem with the fetch operation:', error);
  });
  1. Discuss the importance of checking response status and converting the response to JSON.
  • Checking Response Status: It’s important to check the response status to ensure the request was successful. The response.ok property is true if the response status is between 200 and 299, indicating success. If the response status indicates an error (e.g., 404, 500), you should handle it appropriately by throwing an error or using custom logic.

  • Converting Response to JSON: Most APIs return data in JSON format. The response.json() method reads the response body and parses it as JSON. This is a common operation since it allows you to work with the response data as a JavaScript object, making it easier to manipulate and use within your application.

  1. Highlight the need for proper error handling using throw and catch in fetch operations.

    Proper error handling is crucial in fetch operations to ensure your application can gracefully handle network issues, server errors, or unexpected response formats. By using throw and catch, you can manage these errors and provide appropriate feedback or fallback functionality.

In the fetch example above:

• If the response is not OK (!response.ok), we throw an error. This ensures that any unsuccessful HTTP response (like 404 or 500) is caught and handled in the catch block.

• The catch block then logs the error or performs any necessary error handling. This helps in debugging and provides a way to inform the user or take corrective actions when something goes wrong.

  1. Describe how chaining allows sequential execution of asynchronous tasks.

    Chaining promises allows you to perform asynchronous tasks in a sequential manner. Each then method returns a new promise, which can be chained to handle the result of the previous promise. This ensures that each step is executed only after the previous step has completed, allowing for ordered execution of asynchronous operations.

  2. Explain the flow of data from one then block to another.

    When you chain then methods, the value returned from one then block is passed as the input to the next then block. If a then block returns a promise, the next then block waits for that promise to resolve before continuing. This creates a flow where data or results from one step are seamlessly passed to the next, enabling complex sequences of dependent asynchronous tasks.

     fetchUserData(userId)
       .then((userData) => {
         console.log('User Data:', userData);
         return fetchUserPosts(userData.username); // Return the next promise
       })
       .then((userPosts) => {
         console.log('User Posts:', userPosts); // Handle the result of the next promise
       })
       .catch((error) => {
         console.error('Error:', error); // Handle any error that occurs in the chain
       });
    
  3. Use this example to demonstrate fetching user data from an API and then performing additional operations based on the fetched data.

     // Function to fetch user data based on userId
     function fetchUserData(userId) {
         return new Promise((resolve, reject) => {
             fetch(`https://api.freeapi.app/api/v1/public/randomusers/${userId}`)
                 .then(response => {
                     if (!response.ok) {
                         throw new Error('Network response was not ok');
                     }
                     return response.json();
                 })
                 .then(data => {
                     resolve(data);
                 })
                 .catch(error => {
                     reject('Failed to fetch user data: ' + error.message);
                 });
         });
     }
    
     // Function to simulate processing user data (e.g., fetch user posts)
     function fetchUserPosts(username) {
         return new Promise((resolve, reject) => {
             setTimeout(() => {
                 if (username) {
                     resolve(['Post 1 by ' + username, 'Post 2 by ' + username, 'Post 3 by ' + username]);
                 } else {
                     reject('Username is missing');
                 }
             }, 1000);
         });
     }
    
     const userId = '1'; // Example userId
    
     fetchUserData(userId)
         .then((userData) => {
             console.log('User Data:', userData);
             return fetchUserPosts(userData.username);
         })
         .then((userPosts) => {
             console.log('User Posts:', userPosts);
         })
         .catch((error) => {
             console.error('Error:', error);
         });
    
  4. Emphasize how this pattern is common in web development for tasks like data retrieval and processing.

    This pattern is very common in web development, especially when dealing with APIs and asynchronous data fetching. Typical use cases include:

    Fetching User Data: Getting user information from a backend service and then using that data to perform further operations, such as fetching related resources (e.g., posts, comments).

    Sequential API Calls: Making a series of dependent API calls, where the result of one call is required for the next. For example, fetching a list of users and then fetching detailed information for each user.

    Data Processing: Fetching data from an API and then transforming or processing that data before displaying it to the user.

Final Words:

Promises in JavaScript are key for handling asynchronous operations, such as fetching data from APIs. The fetch API allows for making HTTP requests and returns promises, enabling clean and sequential handling of tasks. Proper error handling using throw and catch ensures robustness by managing issues gracefully. Chaining promises helps in executing tasks in order, making the code readable and maintainable. The example of fetching user data and posts demonstrates real-world usage. Understanding and explaining these concepts in interviews showcases your ability to handle asynchronous tasks effectively, which is crucial for modern web development.

1
Subscribe to my newsletter

Read articles from Sanjeev Singh directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Sanjeev Singh
Sanjeev Singh