Build a JavaScript Promise Coding Interview Question | Skilled.dev
Interview Question

Build a JavaScript Promise

Course launching November 17, 2020

Follow me on YouTube for free coding interview videos.

Users who sign up for the email list will receive an exclusive 75% discount at launch.

Your subscription could not be saved. Please try again.
Your subscription was successful! šŸ¤“
Loading...

Building a JavaScript Promise implementation has become an increasingly popular interview question. It tests your understanding of asynchronous JS and promises at a fundamental level. It also shows your can effectively build objects and implement powerful patterns like chaining.

Asynchronous programming is a core concept in JavaScript and is a feature that gives JavaScript a lot of speed compared to other scripted programming languages. JavaScript is single-threaded which means it executes programs line-by-line. It is also asynchronous which means that if our program execution reaches a block of code that must wait on a result, it will continue on past this block of code that is waiting so the program doesn't freeze execution, and once that async task finishes, our code will handle the result it was waiting for by using a promise or a callback.

Since the release of ES2015, the most common way to handle the result of an async function call is through a Promise, but before that callbacks were used. Recently, the async/await syntax was added to JavaScript, but this is just an abstraction on top of promises, and we'll see how they compare at the end of the article.

In this lesson, you will learn JavaScript promises by building a JavaScript promise implementation from scratch. Your promise will be declared exactly like they are in native JavaScript new Promise(...), and it will be able to chainĀ .then() andĀ .catch() statements to handle the result of async code.

If the below problem prompt is confusing and seems too complex, don't worry because it will all make sense as you follow the solution.

Create a constructor PromiseSimple that allows you to create object to handle asynchronous actions using the then and catch keywords which should also be chainable. Your constructor will take a function as an argument that will resolve or reject the async function request new PromiseSimple((resolve, reject) => {}) and initiates the promise chain.

class PromiseSimple {
  constructor() {...}
  // Should at least have these two functions
  then(fnOnResolve) {...}
  catch(fnOnReject) {...}

  // ...likely will have other help functions
}

const makeApiCall = () => {
  return new PromiseSimple((resolve, reject) => {
    // Perform async action...
    const apiResponse = request('/api');

    if (apiResponse.statusCode >= 400) {
      reject(apiResponse);
    } else {
      resolve(apiResponse.data);
    }
  });
};

makeApiCall()
  .then((response) => /* do something with response */)
  .then((response) => /* do something else with response */)
  .then((response) => /* do one last thing with response */)
  .catch((err) => /* handle error */)

Implement a Promise

The JavaScript promise example we are building is meant to help you understand the basics of promises and asynchronous thinking - it is not intended to show the most optimized version of a promise. I will first describe promises for beginners and then dive into our own promise implementation so we can understand it from the ground up.

You've probably seen something like this before:

fetch('/user/1')
  .then((user) => {
    /* Do something with user after the API returns */
  })

The block of code in theĀ .then() waits until it receives the response from the server before it executes anything. This is called a Promise. But don't let the fancy name or the fact that there is asynchronous code intimidate you - a Promise is just a plain old JavaScript object with special methods that allow you to execute code synchronously after the promise resolve (it will do things in order even though there is a delay).

typeof new Promise((resolve, reject) => {}) === 'object' // true

Let me reiterate (because this is something that was difficult for me to grasp when I first learned promises), a Promise is just an object.

To be able to wait on the server and execute the code in theĀ .then() chain after the response, you MUST return a Promise object. This is not something functions get out of the box. Behind the scenes, the fetch function is doing something like this.

const fetch = function(url) {
  return new Promise((resolve, reject) => {
    request((error, apiResponse) => {
      if (error) {
        reject(error)
      }

      resolve(apiResponse)
    })
  })
}

The fetch() function makes an http request to the server, but the client doesn't know when the server will send back the results. So JavaScript begins executing other unrelated code while waiting on the server to return with the response. Once the client receives the response, it initiates the execution of the code in theĀ .then() statements by calling resolve(apiResponse).

Now let's take a closer look at how the Promise actually allows you to do this.

class PromiseSimple {
  constructor(executionFunction) {
    this.promiseChain = [];
    this.handleError = () => {};

    this.onResolve = this.onResolve.bind(this);
    this.onReject = this.onReject.bind(this);

    executionFunction(this.onResolve, this.onReject);
  }

  then(handleSuccess) {
    this.promiseChain.push(handleSuccess);

    return this;
  }

  catch(handleError) {
    this.handleError = handleError;

    return this;
  }

  onResolve(value) {
    let storedValue = value;

    try {
      this.promiseChain.forEach((nextFunction) => {
        storedValue = nextFunction(storedValue);
      });
    } catch (error) {
      this.promiseChain = [];

      this.onReject(error);
    }
  }

  onReject(error) {
    this.handleError(error);
  }
}

NOTE: This version of a Promise is for educational purposes only. I've left out some of the more advanced features and distilled it to its core functionality.

I've named it PromiseSimple so it won't clash with the native Promise in case you want to copy and paste it into your Chrome console. Our promise implementation has a constructor, 2 public methods that you may be familiar with then() and catch(), and 2 internal methods onResolve() and onReject().

When you create a promise, you do so like this new Promise((resolve, reject) => {/*Ā ... */}). You pass it a callback function which I've named executionFunction in the constructor. The execution function takes a resolve and reject which map to the internal onResolve() and onReject() function. These are the functions that will be called when the fetch calls the resolve or reject.

The constructor also creates a promiseChain array and handleError function. When a series ofĀ .then(() => {}) are added, it pushes each handleSuccess function onto the promiseChain. When a user calls catch(() => {}), it assigns the function to the internal handleError. Notice that the then() and catch() function return this;. This allows you to chain multiple then()'s since you're returning the object itself.

NOTE: In the native Promise, these then() and catch() functions actually return a new Promise themselves, but for this simple scenario, I've only returned this. In addition, there can be multipleĀ .catch() blocks and they can be chained as well and aren't required to come at the end of theĀ .then() chain.

When your asynchronous function calls resolve(apiResponse), the promise object then begins executing onResolve(apiResponse). It iterates through the entire promiseChain by removing the function at the front and executes it with the most recent value saved in storedValue. It then updates storedValue to the result of the most recent execution. It will execute these functions in order. This creates the synchronous promise chain.

This loop is wrapped in a try/catch block. This is special JavaScript syntax that looks for errors. If your asynchronous function calls reject(error) or your try/catch recognizes an error, it will then be passed to the onReject() method which calls the function that you passed toĀ .catch().

NOTE: If you want to see our promise implementation working with a real API request, check out the REPL from the solution video.

Putting it all together with a more practical example:

class PromiseSimple {
  constructor(executionFunction) {
    this.promiseChain = [];
    this.handleError = () => {};

    this.onResolve = this.onResolve.bind(this);
    this.onReject = this.onReject.bind(this);

    executionFunction(this.onResolve, this.onReject);
  }

  then(handleSuccess) {
    this.promiseChain.push(handleSuccess);

    return this;
  }

  catch(handleError) {
    this.handleError = handleError;

    return this;
  }

  onResolve(value) {
    let storedValue = value;

    try {
      this.promiseChain.forEach((nextFunction) => {
        storedValue = nextFunction(storedValue);
      });
    } catch (error) {
      this.promiseChain = [];

      this.onReject(error);
    }
  }

  onReject(error) {
    this.handleError(error);
  }
}

fakeApiBackend = () => {
  const user = {
    username: 'treyhuffine',
    favoriteNumber: 42,
    profile: 'https://gitconnected.com/treyhuffine'
  };

  // Introduce a randomizer to simulate the
  // the probability of encountering an error
  if (Math.random() > .05) {
    return {
      data: user,
      statusCode: 200,
    };
  } else {
    const error = {
      statusCode: 404,
      message: 'Could not find user',
      error: 'Not Found',
    };

    return error;
  }
};

// Assume this is your AJAX library. Almost all newer
// ones return a Promise Object
const makeApiCall = () => {
  return new PromiseSimple((resolve, reject) => {
    // Use a timeout to simulate the network delay waiting for the response.
    // This is THE reason you use a promise. It waits for the API to respond
    // and after received, it executes code in the `then()` blocks in order.
    // If it executed is immediately, there would be no data.
    setTimeout(() => {
      const apiResponse = fakeApiBackend();

      if (apiResponse.statusCode >= 400) {
        reject(apiResponse);
      } else {
        resolve(apiResponse.data);
      }
    }, 5000);
  });
};

makeApiCall()
  .then((user) => {
    console.log('In the first .then()');

    return user;
  })
  .then((user) => {
    console.log(`User ${user.username}'s favorite number is ${user.favoriteNumber}`);

    return user;
  })
  .then((user) => {
    console.log('The previous .then() told you the favoriteNumber')

    return user.profile;
  })
  .then((profile) => {
    console.log(`The profile URL is ${profile}`);
  })
  .then(() => {
    console.log('This is the last then()');
  })
  .catch((error) => {
    console.log(error.message);
  });

async/await

The async/await syntax is just a wrapper around promises. If you mark a function as async, it just converts the return response to a promise.

const apiRequest = async () => {
  // do some async things for an unknown amount of time...
  return { answer: 42 } ;
}

const run = async () => {
  const response = await apiRequest();
  console.log(response);
}

This is the equivalent of doing the following:

const apiRequest = () => {
  return new Promise((resolve) => {
    // do some async things for an unknown amount of time...
    return resolve({ answer: 42 });
  })
}

const run = () => {
  apiRequest.then((response) => {
    console.log(response);
  })
}

// Since it's all just promises, you can mix and match with async/await
const runAsAsyncAwait = async () => {
  const response = await apiRequest();
  console.log(response);
}
Prev
Array Methods
Next
Pub-Sub and Event-Driven Programming
Loading...

Table of Contents