JavaScript questions I find very useful in the Interviews.

JavaScript questions I find very useful in the Interviews.

Part 2

[12]

Preface

We're continuing from our last article, which you can read here. It took me a while to come up with the second part, lol I'm sorry work schedule has been a bit crazy. I'm concentrating on questions related to Promises in JavaScript for this article, instead of wandering around. So, let's get started.

So, what are Promises?

JavaScript promises are pretty much like promises you'd make to your mom or dad, but it's not quite that easy.

It's like rather than getting super hopeful that the other person will keep the promise, you can handle both situations here. The one when the promise is kept and when it isn't.

I promise to do this whenever that is true. If it isn’t true, then I won’t.

Technically, this sounds more like an IF statement in any programming language but it's a bit more than that. So, here's the spiel - A Promise is just a fanciful way to defer some tasks for the future — in some cases you don't exactly know how things will turn out, so you need to be prepared for both the good and the bad. You write code to handle both the scenarios: when the promise is kept and when it isn’t. For instance, a promise helps you manage the asynchronous nature of an operation. JavaScript is not designed to wait around for the asynchronous tasks to finish before moving on to execution of the synchronous parts. Like, when you make API requests to some servers, you don’t know if the servers are online or offline, or how long they’ll take to respond to the request.

Promise.any()

Question 1: Implement the functionality behavior of Promise.any()

The Promise.any() method is one of the promise concurrency methods. Promise.any() is all about grabbing the first win. It races all the promises to see which one fulfills first, then stops waiting for the rest. Unlike Promise.all(), which returns an array of fulfillment values, we only get one fulfillment value assuming at least one promise fulfills. Remember, Promise.any() throws an error if you give it an empty list of promises (iterable). It can't find any winners in an empty race! This behavior aligns with Array.prototype.some() which also returns false for an empty array.

Also, don't confuse Promise.any() with Promise.race(). While both involve a "race", they have different goals. Promise.race() is more like a "first come, first served" race. It returns the result of the first promise to settle, regardless of whether it's fulfilled or rejected and Promise.any() is more like a "first win" race. It only cares about the first promise to fulfill and ignores all rejections until a winner emerges.

Syntax

Promise.any(iterable)

Parameters

iterable: An iterable can be considered as an array of promises.

Return Value

It returns a single Promise object when it resolves or rejects in the following cases —

  • If you provide an empty list of promises (when the iterable is empty), the returned Promise is rejected immediately, unlike Promise.allSettled().

  • If any of the promises in the list fulfills, the returned Promise immediately resolves with the value of that first fulfilled promise. It doesn't wait for the others.

  • If all the promises in the list are rejected, the returned Promise is eventually rejected with an AggregateError. This error object contains an array named errors which holds the reason (error) for each rejected promise, in the order they were provided.

One thing to note here is that, even if the provided list contains no pending promises (all are already settled, fulfilled or rejected), the returned Promise is still asynchronously rejected and not synchronously. This might seem counterintuitive, but it maintains consistency with the asynchronous nature of promises.

Example

const pErr = new Promise((resolve, reject) => {
  reject("Always Fails")
})

const pSlow = new Promise((resolve, reject) => {
  setTimeout(resolve, 500, "Done eventually");
})

const pFast = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, "Done Quick")
})

Promise.any([pErr, pSlow, pFast]).then((value) => {
  console.log(value) // pFast fulfills first
})

If none of the promises in Promise.any() fulfill, it throws an AggregateError encapsulating the rejection reasons of all the promises.

const pErr = new Promise((resolve, reject) => {
  reject("Always Fails")
})

// const pSlow = new Promise((resolve, reject) => {
//   setTimeout(resolve, 500, "Done eventually");
// })

// const pFast = new Promise((resolve, reject) => {
//   setTimeout(resolve, 100, "Done Quick")
// })

Promise.any([pErr]).catch((value) => {
  console.log(value) // [AggregateError: All promises were rejected]
})

Solution

To implement the functionality behavior of Promise.any(), we need to keep track of the number of rejections and resolve the main promise as soon as any promise fulfills.

function promiseAny(promises) {
    return new Promise((resolve, reject) => {
        if (promises.length === 0) {
            return reject(new AggregateError([], "All promises were rejected"))
        }

        let rejectionCount = 0;
        const errors = []

        promises.forEach((promise, index) => {
            Promise.resolve(promise).then(
                value => resolve(value),
                error => {
                    rejectionCount++
                    errors[index] = error
                    if (rejectionCount === promises.length) {
                        reject(new AggregateError(errors, "All promises were rejected"))
                    }
                }
            )
        })
    })
}

const promise1 = Promise.reject(new Error("Promise 1 failed"))
const promise2 = new Promise((resolve) => setTimeout(resolve, 100, "Promise 2 succeeded"))
const promise3 = Promise.reject(new Error("Promise 3 failed"))

promiseAny([promise1, promise2, promise3])
    .then(value => console.log(value))  // Expected output: "Promise 2 succeeded"
    .catch(error => console.error(error))

Logic —

If the array of promises is empty, we immediately reject with an AggregateError indicating that all promises were rejected (since there were none). Using rejectionCount to count the number of rejected promises and an array errors to store the errors.

For each promise, we use Promise.resolve(promise) to handle non-promise values, just like Promise.any() does. If a promise fulfills, we call resolve with it's value. If a promise is rejected, we increment the rejectionCount and store the corresponding error. If all promises have been rejected, we call reject with an AggregateError containing all errors.

Promise.allSettled()

Question 2: Implement the functionality behavior of Promise.allSettled()

Promise.allSettled() is a static method that acts on an iterable of promises (like an array of promises). It returns a single promise that resolves when all the input promises have settled (either fulfilled or rejected). This provides a way to handle both successful and failed promises within a single operation.

The Promise.allSettled() method is also one of the promise concurrency methods. Promise.allSettled() is a tool for handling multiple asynchronous tasks at once. It's particularly useful when the tasks are independent that is the success or failure of one task doesn't affect the others. Another scenarios is when you need results from all tasks and you want to know how each task ended.

Syntax

Promise.allSettled(iterable)

Parameters

iterable: An iterable can be considered as an array of promises.

Return Value

It returns a single Promise object that is —

  • If you provide an empty list of promises (when the iterable is empty), the returned Promise resolves immediately (already fulfilled).

  • Otherwise, it waits for all the promises in the list to settle (either fulfilled or rejected). Once all are settled, the returned Promise resolves.

There will be one object for each promise you provided in the initial list. and each outcome object will have two properties:

  • status: A string that indicates whether the corresponding promise was 'fulfilled' or 'rejected'.

  • value (optional): Present only if the promise was 'fulfilled'. It contains the value the promise resolved with.

  • reason (optional): Present only if the promise was 'rejected'. It contains the reason (error) for the rejection.

Example

Promise.allSettled([
  Promise.resolve(33),
  new Promise((resolve) => setTimeout(() => resolve(66), 0)),
  99,
  Promise.reject(new Error("Some Error")),
]).then((values) => console.log(values) )

The above code will return the following output —

[
  { status: 'fulfilled', value: 33 },
  { status: 'fulfilled', value: 66 },
  { status: 'fulfilled', value: 99 },
  {
    status: 'rejected',
    reason: Error: Some Error
  }
]

{ status: 'fulfilled', value: 33 }: The first promise was resolved with the value 33.

{ status: 'fulfilled', value: 66 }: The second promise was resolved with the value 66.

{ status: 'fulfilled', value: 99 }: The third promise was resolved with the value 99.

{ status: 'rejected', reason: Error: An Error }: The fourth promise was rejected with the error message "Some Error".

Solution

function customIsAllSettled(promises) {
  return new Promise((resolve) => {
    const results = [];
    let settledCount = 0;

    promises.forEach((promise, index) => {
      Promise.resolve(promise) // Using Promise.resolve() to ensure the current item is treated as a promise
        .then(value => {
          results[index] = { status: 'fulfilled', value };
        })
        .catch(reason => {
          results[index] = { status: 'rejected', reason };
        })
        .finally(() => {
          settledCount += 1;
          // Once all promises have settled, resolve the main promise with the results
          if (settledCount === promises.length) {
            resolve(results);
          }
        });
    });
  });
}

const promises = [
  Promise.resolve(33),
  new Promise((resolve) => setTimeout(() => resolve(66), 0)),
  99,
  Promise.reject(new Error("An Error")),
];

customIsAllSettled(promises).then((results) => {
  console.log(results);
});

Logic —

We first create an array results to store the result of each promise and initializing settledCount to keep track of the number of the settled promises. For each promise, we use .then to capture the resolved values and .catch to capture the rejected reasons. These results are stored in the results array.

The .finally is used to ensure that the settledCount is incremented whenever each promise in the input array settles (whether it's resolved or rejected). This allows us to keep track of how many promises have settled.

This function emulates Promise.allSettled() by collecting the status (fulfilled/rejected) and values/reasons of all input promises into a single result object.

Promise.resolve()

Question 3: Implement a function to resolve a given value to a Promise.

The Promise.resolve() is another static method that takes any value and turns it into a settled promise. If the value is already a promise, it will simply return that promise. If the value is something special that can act like a promise (a thenable), then Promise.resolve() will use it to create a new promise; otherwise, it creates a new promise that's immediately successful with the given value.

This function takes nested promises (promises that resolve to other promises) and unwraps them all at once. Imagine a box inside a box inside a box, each containing a gift. This function flattens them all out, giving you the final gift (the non-promise value) directly. In the end, you get a single promise that resolves to a regular value, not another promise.

Syntax

Promise.resolve(value)

Parameters

value: This is the data you want the promise to eventually resolve with. It can be any regular value like a number, string, object, etc. Similarly, it can be a existing Promise or a thenable.

Return Value

It returns a single Promise object that resolves with the value you provided (value). The value itself can be a Promise, in that case it just returns that same promise (no changes). One thing to remember is that regardless of the state (fulfilled, rejected, or pending) of the original promise provided as value, the returned promise will always be resolved. It won't inherit the state of the original promise.

Solution

I'll explain with different scenarios —

Using the static Promise.resolve method

Promise.resolve("Success").then(
  (value) => {
    console.log(value); // Success
  },
  (reason) => {
    // Will not be called
  },
);

Resolving an array

const p = Promise.resolve([1, 2, 3]);
p.then((v) => {
  console.log(v[2]); // 3
});

Resolving another Promise

const original = Promise.resolve(9);
const cast = Promise.resolve(original);
cast.then((value) => {
  console.log(`value: ${value}`);
});
console.log(`original === cast ? ${original === cast}`);
// Logs, in order:
// original === cast ? true
// value: 9

In this case, original === cast will always be true because Promise.resolve() is smart. If you give it a promise already, it just hands you back the same promise, not a new one. Also, the logs might appear out of order because then() handlers run asynchronously.

Final Implementation —

function resolveToPromise(value) {
  // Checking if the value is a Promise
  if (value instanceof Promise) {
    return value;
  }

  // Otherwise, wrapping the value in a resolved Promise
  return Promise.resolve(value);
}

// Passing a non-Promise value
const nonPromiseValue = 42;
resolveToPromise(nonPromiseValue).then(result => console.log(result)); // Output: 42

// Passing a Promise
const promiseValue = new Promise((resolve) => resolve("Hello, world!"));
resolveToPromise(promiseValue).then(result => console.log(result)); // Output: Hello, world!

// Passing a rejected Promise
const rejectedPromise = Promise.reject(new Error("Oops!"));
resolveToPromise(rejectedPromise).catch(error => console.error(error)); // Output: Error: Oops!

This utility function guarantees that anything you throw it's way either data or a Promise gets converted into a Promise. This makes it easier to work with both synchronous and asynchronous values in a uniform way.

Promisification

Question 4: Implement a promisify function that allows the original function to override the return value.

Tired of callback chaos? Enter promisification.

Imagine you have a function that relies on callbacks to tell you when it's done. Callbacks can get messy, especially when you chain them together. That's where promisification comes in. Since many libraries still use callbacks, promisification becomes your superhero tool to tame the asynchronous beast and write smoother code.

Solution

Implementing a promisify function in JavaScript that allows the original function to override the return value for promises.

function promisify(fn) {
  return function(...args) {
    return new Promise((resolve, reject) => {
      const callback = (err, result) => {
        if (err) {
          return reject(err);
        }

        // Checking for the override flag in the result object
        if (result && result.override) {
          resolve(result.override);
        } else {
          resolve(result);
        }
      };

      // Calling the original function with callback and arguments
      args.push(callback);
      fn.apply(this, args);
    });
  };
}

function someAsyncFunction(arg1, arg2, callback) {
  setTimeout(() => {
    const error = Math.random() < 0.5 ? new Error("Oops!") : null;
    const result = { data: "Some data", override: "Overridden value" };
    callback(error, result);
  }, 1000);
}

const promisedFunction = promisify(someAsyncFunction);

promisedFunction("arg1", "arg2")
  .then(result => console.log(result)) // Output: "Overridden value"
  .catch(err => console.error(err));

The promisify function returns a new function that accepts any number of arguments (...args). Inside the new function, a Promise is created with resolve and reject functions (an executor function). Now, inside the executer function, a callback is defined which is passed to the original function (fn). The callback also checks if there was an error (err). If the error exists, the Promise is rejected otherwise, it checks if the result contains an override property. If it is true, the Promise is resolved with the overridden value; otherwise, the Promise is resolved with the result.

The function someAsyncFunction is a demo asynchronous function that uses a callback. The asynchronous behavior is simulated using setTimeout, after 1 second, the original function executes the callaback and if error is generated, Promise will be rejected else resolved with either the overridden value or the original result.

Promisification rocks for async code, especially with async/await. But it doesn't fully replace callbacks. Promises are one-shot deals (resolve or reject once), while callbacks can be called multiple times. So promisify functions with single-use callbacks for smoother async coding.

Conclusion

As a JavaScript developer, you should know what a promise is and how it works internally, which will also help you in JS interviews. All right, that's it for this article. We'll continue in the next part with more questions, promise ;)