Hone logo
Hone
Problems

Build Your Own Promise: A JavaScript Asynchronous Foundation

The native Promise object in JavaScript is fundamental for handling asynchronous operations. Understanding its internal workings is crucial for mastering asynchronous programming. This challenge asks you to implement a custom Promise class from scratch, replicating the core behavior of the native Promise. This will deepen your understanding of callback management, state transitions, and asynchronous execution.

Problem Description

You are tasked with creating a MyPromise class in JavaScript that mimics the behavior of the built-in Promise. This class should handle asynchronous operations by maintaining a state (pending, fulfilled, or rejected) and executing callbacks based on that state.

Key Requirements:

  1. Constructor (MyPromise(executor)):

    • The constructor should accept an executor function.
    • The executor function is immediately invoked with two arguments: resolve and reject.
    • resolve(value): Transitions the promise to the "fulfilled" state with the given value.
    • reject(reason): Transitions the promise to the "rejected" state with the given reason.
    • The promise should start in the "pending" state.
  2. State Management:

    • The promise should have three possible states: pending, fulfilled, and rejected.
    • Once a promise is fulfilled or rejected, its state and value/reason cannot be changed.
  3. .then(onFulfilled, onRejected) Method:

    • This method registers callbacks to be executed when the promise is settled.
    • onFulfilled: A function to be called if the promise is fulfilled. It receives the fulfillment value as an argument.
    • onRejected: A function to be called if the promise is rejected. It receives the rejection reason as an argument.
    • If the promise is already settled when .then() is called, the appropriate callback should be executed asynchronously (using setTimeout or a similar mechanism to simulate microtask queuing).
    • .then() itself should return a new promise. This returned promise should:
      • Be fulfilled with the return value of onFulfilled or onRejected if they return a non-promise value.
      • Be fulfilled or rejected based on the settlement of the promise returned by onFulfilled or onRejected (if they return a promise).
      • Be rejected if onFulfilled or onRejected throws an error.
  4. .catch(onRejected) Method:

    • This is a shorthand for .then(null, onRejected). It registers a callback to handle rejections.
    • .catch() should also return a new promise.
  5. Asynchronous Execution:

    • All callbacks (resolve, reject, onFulfilled, onRejected) should be executed asynchronously. This means they should not run immediately when called, but rather be queued for execution on the next tick of the event loop. setTimeout(callback, 0) is a common way to achieve this for this challenge.

Expected Behavior:

  • Promises should correctly transition through states.
  • Callbacks should be executed in the correct order and with the correct arguments.
  • Chaining of promises via .then() should work as expected.
  • Handling of rejections via .catch() and the onRejected argument of .then() should function properly.
  • Returning promises from .then() handlers should correctly propagate their settlement.

Edge Cases to Consider:

  • Calling resolve or reject multiple times on the same promise.
  • Calling .then() after the promise has already been settled.
  • Returning promises from .then() handlers.
  • Throwing errors within .then() handlers.
  • Passing non-function values to .then() and .catch().

Examples

Example 1: Basic Fulfillment

const myPromise = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve('Success!');
  }, 100);
});

myPromise.then(
  (value) => {
    console.log('Fulfilled:', value); // Expected: Fulfilled: Success!
  },
  (reason) => {
    console.error('Rejected:', reason);
  }
);

Explanation: The executor function calls resolve('Success!') after a 100ms delay. The onFulfilled callback in .then() is then executed asynchronously, logging "Fulfilled: Success!".

Example 2: Basic Rejection

const myPromise = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    reject('Failure!');
  }, 100);
});

myPromise.then(
  (value) => {
    console.log('Fulfilled:', value);
  },
  (reason) => {
    console.error('Rejected:', reason); // Expected: Rejected: Failure!
  }
);

myPromise.catch((reason) => {
  console.error('Caught in catch:', reason); // Expected: Caught in catch: Failure!
});

Explanation: The executor function calls reject('Failure!') after a 100ms delay. Both the onRejected callback in .then() and the .catch() handler are executed asynchronously, logging the rejection reason.

Example 3: Chaining and Promise Return

const myPromise = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve(10);
  }, 100);
});

myPromise
  .then((value) => {
    console.log('First then:', value); // Expected: First then: 10
    return value * 2; // Returns a non-promise value
  })
  .then((value) => {
    console.log('Second then:', value); // Expected: Second then: 20
    return new MyPromise((resolve) => setTimeout(() => resolve(value + 5), 50)); // Returns a promise
  })
  .then((value) => {
    console.log('Third then:', value); // Expected: Third then: 25
  })
  .catch((error) => {
    console.error('Error:', error);
  });

Explanation: The first .then() receives 10, returns 20. The second .then() receives 20, and returns a new promise that resolves to 25 after 50ms. The third .then() receives 25 and logs it. The chaining works because each .then() returns a new promise that resolves based on the previous step.

Example 4: Error Handling in then

const myPromise = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve('Initial Value');
  }, 100);
});

myPromise
  .then((value) => {
    console.log('First then:', value); // Expected: First then: Initial Value
    throw new Error('Something went wrong in then');
  })
  .then((value) => {
    console.log('Second then:', value); // This won't be called
  })
  .catch((error) => {
    console.error('Caught error:', error.message); // Expected: Caught error: Something went wrong in then
  });

Explanation: The first .then() handler executes, logs the value, and then throws an error. This error causes the promise returned by the first .then() to be rejected. The .catch() handler then intercepts this rejection.

Constraints

  • Your MyPromise class should be written in plain JavaScript.
  • No external libraries or polyfills for Promises are allowed.
  • You must use setTimeout(callback, 0) (or a similar browser-native non-blocking mechanism) to ensure asynchronous execution of callbacks.
  • The implementation should aim for correctness and robustness, handling the specified behaviors and edge cases.

Notes

  • Consider how to store the state, the value/reason, and the pending callbacks.
  • The core logic will involve checking the promise's state when .then() or .catch() is called, and when resolve() or reject() are invoked.
  • The returned promise from .then() needs to be carefully managed, linking its settlement to the outcome of the handlers or the returned promise from those handlers.
  • Remember that the executor function is called immediately upon new MyPromise().
  • Pay close attention to the "thenable" concept when handling promises returned from .then() callbacks. A "thenable" is an object with a .then() method. Your implementation should be able to handle both native Promises and instances of MyPromise returned from handlers.
Loading editor...
javascript