2776. Convert Callback Based Function to Promise Based Function


Problem Description

The given problem outlines the requirement to transform a conventional callback-based function into a promise-based function in JavaScript. A callback-based function typically takes a callback as its first argument, with subsequent arguments being the data or parameters it operates on. The callback function usually has two parameters: the first is the error (if any), and the second is the result of the operation.

The promisify function we need to write should accept a callback-based function fn and return a new function that, instead of using callbacks, returns a promise. This promise should resolve if the original function's operation is successful, passing the result as the resolution value, or reject if an error occurs, passing the error as the rejection reason.

For instance, if we have a function sum which adds two numbers but only if they're positive, it would call the callback with the sum if the numbers are positive or with an error if any of the numbers are negative. The objective is to create a promisified version of such a function, so instead of dealing with callbacks, we can work with the more modern promise structure that allows chaining and better error handling with try-catch blocks in async functions or with .then().catch() chains.

Intuition

The intuition behind converting a callback-based function to a promise-based one lies in understanding the behavior of callbacks and promises. Callbacks are functions passed into other functions as arguments to be executed later, often upon the completion of an asynchronous operation. Promises are objects that represent the eventual completion or failure of an asynchronous operation and its resulting value.

When "promisifying" a function, we are essentially encapsulating the callback mechanism within the promise's resolve (for success) and reject (for failure) methods. By doing so, we create a function that no longer requires a callback function as a parameter and instead returns a promise that can be handled using .then() for the resolved value or .catch() for the rejected reason.

The solution code uses TypeScript, which allows specifying types for better code reliability and maintainability. The provided promisify function returns an async function that, when called, returns a Promise. In the body of this function, the original function fn is called with a special wrapped callback that invokes resolve when the operation is successful or reject when an error is passed to the callback. The rest of the arguments are directly passed to the original function with the help of the spread operator, ...args.

This approach decouples the original function's logic from the handling of asynchronous execution, providing a more flexible and modern pattern for managing asynchronous operations in JavaScript.

Not Sure What to Study? Take the 2-min Quiz to Find Your Missing Piece:

Which two pointer techniques do you use to check if a string is a palindrome?

Solution Approach

The solution approach for the promisify function involves wrapping the callback-based function with a new function that returns a Promise. We're using a closure here, a powerful feature in JavaScript where an inner function has access to the variables of its enclosing scope, to retain access to the original function fn and its arguments.

Here are the steps the promisify function performs:

  1. Creating a New Function: The promisify function returns a new async function, which when called, will execute the original callback-based function fn. As async functions always return a promise, it sets up a foundation for promisification.

  2. Returning a Promise: Within the returned function, we construct a new Promise using the new Promise constructor. The promise constructor takes an executor function with two parameters: resolve and reject. These are functions that change the state of the promise: resolve will fulfill the promise, and reject will reject the promise.

  3. Executing the Original Function: We then call the original function fn with a custom callback and the rest of the arguments. This custom callback adheres to the Node.js style of callbacks, where the first argument is an error, and the subsequent arguments represent successful response data.

  4. Handling the Callback Response:

    • If the callback is called with an error as the second argument, we invoke the reject function, passing in the error. This changes the state of the promise to rejected and allows downstream error handling with .catch() or a try-catch block in an async function.
    • If the callback is called without an error, we pass the response data to the resolve method. This resolves the promise with the given value, which can be accessed through the .then() method on the returned promise.
  5. Spreading Arguments: The returned function uses the spread operator ...args to pass all provided arguments to the original function fn, after the callback. This spread operator ensures that however many arguments are provided, they are passed on correctly.

The TypeScript types CallbackFn and Promisified describe the shape of the functions being dealt with:

  • CallbackFn represents the original callback-based function. It accepts a callback function as its first argument followed by any number of number arguments.
  • Promisified represents the new function that returns a promise and takes the same number of number arguments as CallbackFn (minus the callback).

The algorithm does not use complex data structures but relies on higher-order functions, closures, and the JavaScript promise mechanism to achieve the desired functionality. The algorithm is rather straightforward and does not involve any changes to the original function's logic but simply wraps it to provide a different interface.

Discover Your Strengths and Weaknesses: Take Our 2-Minute Quiz to Tailor Your Study Plan:

Which algorithm is best for finding the shortest distance between two points in an unweighted graph?

Example Walkthrough

Consider a simple callback-based function calculateArea, which accepts a callback, length, and width. It calculates the area of a rectangle if both length and width are positive numbers. Otherwise, it returns an error through the callback:

1function calculateArea(callback, length, width) {
2    if (length <= 0 || width <= 0) {
3        callback(new Error('Length and width must be positive numbers'), null);
4    } else {
5        callback(null, length * width);
6    }
7}

To demonstrate the solution approach, we will promisify this calculateArea function using the steps provided in the solution approach:

  1. Creating a New Function: We would define promisify to return an async function that invokes calculateArea.

  2. Returning a Promise: Within this new function, we encapsulate the logic of calculateArea by returning a new Promise.

  3. Executing the Original Function: We call calculateArea with a custom callback that properly handles success or error using the promise's resolve and reject functions.

  4. Handling the Callback Response: The custom callback uses the error-first callback pattern to determine how to settle the promise.

  5. Spreading Arguments: Instead of pre-defined parameters, calculateArea takes a list of arguments passed by spread syntax ...args.

Here is the promisified version of calculateArea:

1const promisify = (fn) => {
2    return async (...args) => {
3        return new Promise((resolve, reject) => {
4            fn((error, result) => {
5                if (error) {
6                    reject(error);
7                } else {
8                    resolve(result);
9                }
10            }, ...args);
11        });
12    };
13};
14
15const promisifiedCalculateArea = promisify(calculateArea);
16
17// Now we can use the `promisifiedCalculateArea` function with promises:
18promisifiedCalculateArea(5, 3)
19    .then(area => console.log(`Area: ${area}`))
20    .catch(error => console.error(`Error: ${error.message}`));
21
22// If we try calling it with non-positive numbers, it will handle the error:
23promisifiedCalculateArea(-5, 3)
24    .then(area => console.log(`Area: ${area}`))
25    .catch(error => console.error(`Error: ${error.message}`));

In this example, calling promisifiedCalculateArea(5, 3) will output "Area: 15" because the dimensions are positive. However, promisifiedCalculateArea(-5, 3) will output "Error: Length and width must be positive numbers" because one of the dimensions is not valid.

This walk-through exemplifies how the promisify function can take an existing callback-based function and adapt it to a promise-based pattern without altering the original business logic.

Solution Implementation

1from typing import Callable, Any
2import asyncio
3
4# Define a type for the callback function.
5CallbackFunction = Callable[..., Any]
6
7# Define a type for the resulting promisified function, which returns an awaitable yielding a number.
8PromisifiedFunction = Callable[..., Any]
9
10def promisify(function_with_callback: CallbackFunction) -> PromisifiedFunction:
11    """
12    Converts a callback-based function into a promise-based one.
13  
14    Parameters:
15    function_with_callback (CallbackFunction): The original function that uses a callback.
16  
17    Returns:
18    PromisifiedFunction: A function that returns an awaitable resolving to the same value.
19    """
20
21    # Return a new function that accepts an arbitrary number of arguments.
22    async def wrapper(*args: Any) -> Any:
23        # The new function returns an awaitable.
24        async def inner() -> Any:
25            loop = asyncio.get_running_loop()
26          
27            # Invoke the original function with a callback function.
28            result, error = await loop.run_in_executor(None, lambda: function_with_callback(*args))
29            # If the callback is called with an error, raise an exception.
30            if error:
31                raise Exception(error)
32            # If the callback does not provide an error, return the result.
33            return result
34      
35        return await inner()
36  
37    return wrapper
38
39# Example Usage:
40# Define a function to be promisified.
41def example_function(callback, *args):
42    # Immediately call the callback function with a result value of 42.
43    callback(42, None)
44
45# Promisify the example function.
46async_function = promisify(example_function)
47
48# This is an example of how you might call the promisified function in an async context.
49async def main():
50    # Call the promisified function and print the result.
51    result = await async_function()
52    print(result)  # Expected output: 42
53
54# To actually run the example, you would start the asyncio event loop like this:
55# asyncio.run(main())
56
1import java.util.concurrent.CompletableFuture;
2import java.util.function.BiConsumer;
3
4// Define a functional interface for the callback which will be used inside the method to be promisified.
5@FunctionalInterface
6interface CallbackFunction {
7    void apply(BiConsumer<Integer, String> callback, int... args);
8}
9
10// The promisified method will return a CompletableFuture that can contain the result of type Integer.
11@FunctionalInterface
12interface PromisifiedFunction {
13    CompletableFuture<Integer> apply(int... args);
14}
15
16public class PromisifyExample {
17
18    /**
19     * Converts a callback-based method into a promise-based (CompletableFuture) one.
20     *
21     * @param functionWithCallback The original method that accepts a callback.
22     * @return A method that returns a CompletableFuture resolving to the same value.
23     */
24    public static PromisifiedFunction promisify(CallbackFunction functionWithCallback) {
25        // Return a new method that accepts an arbitrary number of integer arguments.
26        return args -> {
27            // The new method returns a CompletableFuture.
28            CompletableFuture<Integer> future = new CompletableFuture<>();
29            // Invoke the original method with a BiConsumer as the callback function.
30            functionWithCallback.apply((data, error) -> {
31                // If the callback is called with an error, complete the future exceptionally.
32                if (error != null) {
33                    future.completeExceptionally(new RuntimeException(error));
34                } else {
35                    // If there is no error, successfully complete the future with the data.
36                    future.complete(data);
37                }
38            }, args); // Pass the arguments to the original method.
39          
40            return future;
41        };
42    }
43
44    // Example Usage:
45    public static void main(String[] args) {
46        // Promisify a method that uses a callback, which will immediately call the callback with a value of 42.
47        PromisifiedFunction asyncFunction = promisify((callback, arguments) -> callback.accept(42, null));
48        // Invoke the promisified method and log the result to the console.
49        asyncFunction.apply().thenAccept(System.out::println); // Expected output should be 42
50    }
51}
52
1#include <iostream>
2#include <functional>
3#include <future>
4#include <exception>
5
6// Define a type for the callback function which will be wrapped by Promisify.
7using CallbackFunction = std::function<void(std::function<void(int, std::string)>, int)>;
8
9// Define a type for the resulting 'promisified' function, which returns a future of an int.
10using PromisifiedFunction = std::function<std::future<int>(int)>;
11
12/**
13 * Converts a callback-based function into a future-based one.
14 * 
15 * @param function_with_callback The original function that uses a callback.
16 * @returns A function that returns a future resolving to the same value.
17 */
18PromisifiedFunction Promisify(CallbackFunction function_with_callback) {
19    // Return a new function that accepts an integer.
20    return [function_with_callback](int arg) -> std::future<int> {
21        // The new function returns a future.
22        return std::async(std::launch::async, [function_with_callback, arg]() -> int {
23            // Create a promise to hold the result.
24            std::promise<int> promise;
25            std::future<int> result = promise.get_future();
26
27            // Invoke the original function with a callback function.
28            function_with_callback(
29                [&promise](int data, std::string error) {
30                    // If the callback is called with an error, set the exception in the promise.
31                    if (!error.empty()) {
32                        promise.set_exception(std::make_exception_ptr(std::runtime_error(error)));
33                    } else {
34                        // If the callback does not provide an error, set the data in the promise.
35                        promise.set_value(data);
36                    }
37                },
38                arg
39            );
40
41            // Wait for the result to become available and return it.
42            return result.get();
43        });
44    };
45}
46
47// Example usage:
48int main() {
49    // Promisify a function that takes a callback, which will immediately call the callback with a value of 42.
50    auto async_function = Promisify([](auto callback, int) { callback(42, ""); });
51
52    // Invoke the promisified function and log the result to the console.
53    try {
54        std::future<int> future_result = async_function(0);
55        std::cout << "Result: " << future_result.get() << std::endl; // Expected output: 42
56    } catch (const std::exception& ex) {
57        std::cerr << "Error: " << ex.what() << std::endl;
58    }
59
60    return 0;
61}
62
1// Define a type for the callback function which will be wrapped by promisify.
2type CallbackFunction = (next: (data: number, error?: string) => void, ...args: number[]) => void;
3
4// Define a type for the resulting promisified function, which returns a promise of a number.
5type PromisifiedFunction = (...args: number[]) => Promise<number>;
6
7/**
8 * Converts a callback-based function into a promised-based one.
9 * 
10 * @param functionWithCallback The original function that uses a callback.
11 * @returns A function that returns a promise resolving to the same value.
12 */
13function promisify(functionWithCallback: CallbackFunction): PromisifiedFunction {
14    // Return a new function that accepts an arbitrary number of numeric arguments.
15    return async function(...args: number[]): Promise<number> {
16        // The new function returns a promise.
17        return new Promise((resolve, reject) => {
18            // Invoke the original function with a callback function.
19            functionWithCallback((data, error) => {
20                // If the callback is called with an error, reject the promise.
21                if (error) {
22                    reject(error);
23                } else {
24                    // If the callback does not provide an error, resolve the promise.
25                    resolve(data);
26                }
27            }, ...args); // Spread the arguments into the original function.
28        });
29    };
30}
31
32// Example Usage:
33// Promisify a function that takes a callback, which will immediately call the callback with a value of 42.
34// const asyncFunction = promisify((callback) => callback(42));
35// Invoke the promisified function and log the result to the console.
36// asyncFunction().then(console.log); // Expected output: 42
37
Not Sure What to Study? Take the 2-min Quiz:

Which of the following uses divide and conquer strategy?

Time and Space Complexity

The time complexity of the promisify function itself is O(1), meaning it runs in constant time. This is because the promisify function merely constructs and returns a new function without performing any computation that depends on the size of the input.

The provided promisified function will have the time complexity of the original CallbackFn since it is essentially wrapping the existing callback function with promise handling logic. Hence, the time complexity of the promisified function will be the same as the time complexity of the CallbackFn it wraps. If the time complexity of CallbackFn is O(f(n)), where f(n) is a function that describes how the execution time scales with the input size n, then the time complexity of the promisified function is also O(f(n)).

The space complexity of the promisify function is also O(1), or constant space complexity, as it doesn't allocate any additional space that depends on the input size. It just returns a new function object.

The space complexity of the returned promisified function depends on the implementation of the original CallbackFn. However, the use of a promise introduces additional space overhead. This space is for the closure that includes the original CallbackFn, the resolve and reject functions, and any arguments passed to the promisified function. In general, if the space complexity of CallbackFn is O(g(n)), then the space complexity of the created promisified function would be O(g(n)), not including the space used by the Promise internals, which is generally a constant overhead.

Fast Track Your Learning with Our Quick Skills Quiz:

What are the two properties the problem needs to have for dynamic programming to be applicable? (Select 2)


Recommended Readings


Got a question? Ask the Teaching Assistant anything you don't understand.

Still not clear? Ask in the Forum,  Discord or Submit the part you don't understand to our editors.


TA 👨‍🏫