2776. Convert Callback Based Function to Promise Based Function 🔒
Problem Description
This problem asks you to implement a promisify
function that converts callback-based functions into promise-based functions.
The function promisify
takes a single parameter fn
, which is a callback-based function with a specific signature:
- The first parameter of
fn
is always a callback function - Any additional parameters are the actual arguments for the operation
The callback function itself follows the Node.js convention:
- First parameter: the result data (when successful)
- Second parameter: the error (when an error occurs)
Your promisify
function should return a new function that:
- Takes the same arguments as the original function (except the callback)
- Returns a Promise instead of using callbacks
- The Promise resolves with the data if the callback receives data without an error
- The Promise rejects with the error if the callback receives an error
For example, given a callback-based function like:
function sum(callback, a, b) {
if (a < 0 || b < 0) {
const err = Error('a and b must be positive');
callback(undefined, err);
} else {
callback(a + b);
}
}
After promisification, you can use it like:
const promisifiedSum = promisify(sum);
promisifiedSum(2, 3).then(result => console.log(result)); // 5
promisifiedSum(-1, 3).catch(error => console.log(error)); // Error: a and b must be positive
The key challenge is to properly wrap the callback-based function in a Promise constructor, ensuring that the callback's success and error cases are correctly mapped to Promise resolution and rejection.
Intuition
The core insight is that we need to bridge two different asynchronous patterns: callbacks and Promises. Both handle asynchronous operations, but in fundamentally different ways.
Think about what happens in a callback-based function: it performs some operation and then calls a callback with the result. In the Promise world, we want to capture that same result but deliver it through Promise resolution or rejection.
The key realization is that we can use the Promise constructor, which gives us resolve
and reject
functions. We can create our own callback function that will be passed to the original function fn
. When this callback gets invoked by fn
, we decide whether to resolve or reject the Promise based on whether an error was passed.
Here's the mental model:
- When the promisified function is called, we create a new Promise
- Inside this Promise, we call the original callback-based function
fn
- We provide
fn
with a custom callback that acts as a "translator" - This translator callback checks if an error was passed:
- If there's an error (second parameter), we reject the Promise
- If there's no error, we resolve the Promise with the data (first parameter)
The pattern (data, error) => { if (error) reject(error); else resolve(data); }
is the bridge between the two worlds. It takes the callback convention and maps it to Promise behavior.
We wrap everything in a function that returns async function(...args)
to ensure we return a new function that accepts the same arguments as the original (minus the callback), preserving the original function's interface while changing its asynchronous mechanism from callbacks to Promises.
Solution Approach
The implementation follows a higher-order function pattern where promisify
returns a new function that wraps the original callback-based function.
Let's break down the implementation step by step:
-
Function Signature Definition:
function promisify(fn: CallbackFn): Promisified
The function takes a callback-based function and returns a promisified version.
-
Return a New Function:
return async function (...args) { ... }
We return a new function that accepts any number of arguments using the rest parameter
...args
. This preserves the flexibility of the original function's argument list. -
Create and Return a Promise:
return new Promise((resolve, reject) => { ... })
Inside the returned function, we create a new Promise. The Promise constructor gives us
resolve
andreject
functions to control the Promise's outcome. -
Call the Original Function with a Custom Callback:
fn( (data, error) => { if (error) { reject(error); } else { resolve(data); } }, ...args );
We invoke the original function
fn
with:- A custom callback function as the first argument
- All the original arguments spread using
...args
-
The Callback Bridge Logic: The custom callback
(data, error) => { ... }
serves as the bridge between callback and Promise patterns:- It receives
data
(the result) anderror
parameters - If
error
is truthy, it rejects the Promise with that error - If
error
is falsy/undefined, it resolves the Promise with the data
- It receives
The beauty of this solution is that it's generic enough to work with any callback-based function that follows the standard Node.js callback convention. The spreading of arguments (...args
) ensures that functions with varying numbers of parameters can all be promisified using the same implementation.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example to see how the promisify function transforms a callback-based function.
Original callback-based function:
function multiply(callback, x, y) {
if (y === 0) {
callback(undefined, Error('Cannot multiply by zero'));
} else {
callback(x * y);
}
}
Step 1: Apply promisify
const promisifiedMultiply = promisify(multiply);
This returns a new function that accepts (x, y)
instead of (callback, x, y)
.
Step 2: Call the promisified function
promisifiedMultiply(4, 5)
Step 3: Inside the promisified function
When we call promisifiedMultiply(4, 5)
, here's what happens internally:
- The arguments
[4, 5]
are captured via...args
- A new Promise is created
- Inside the Promise constructor, we call the original
multiply
function:multiply( (data, error) => { if (error) reject(error); else resolve(data); }, 4, 5 // spread from ...args )
Step 4: Execution of the original function
The multiply
function runs with:
- First parameter: our custom callback
- Second parameter: 4
- Third parameter: 5
Since y !== 0
, it executes callback(4 * 5)
, which means our custom callback receives:
data = 20
error = undefined
Step 5: Promise resolution Our custom callback checks:
- Is
error
truthy? No (it's undefined) - So we execute
resolve(20)
Final result:
promisifiedMultiply(4, 5).then(result => console.log(result)); // Outputs: 20
Error case example:
promisifiedMultiply(4, 0)
This time multiply
calls callback(undefined, Error('Cannot multiply by zero'))
, so:
data = undefined
error = Error('Cannot multiply by zero')
- Our callback executes
reject(error)
- The Promise rejects with the error
promisifiedMultiply(4, 0).catch(err => console.log(err.message));
// Outputs: "Cannot multiply by zero"
Solution Implementation
1from typing import Callable, Any, Optional
2import asyncio
3
4# Type definition for a callback function that accepts a next callback and variable number arguments
5# The callback function receives data and optional error
6CallbackFn = Callable[[Callable[[Any, Optional[str]], None], *tuple[Any, ...]], None]
7
8# Type definition for the promisified version of the callback function
9# Returns a coroutine that resolves with a value or raises an exception
10Promisified = Callable[[*tuple[Any, ...]], asyncio.Future]
11
12
13def promisify(fn: CallbackFn) -> Promisified:
14 """
15 Converts a callback-based function into a Promise-based function
16
17 Args:
18 fn: The callback-based function to be promisified
19
20 Returns:
21 A new function that returns a Future instead of using callbacks
22 """
23
24 # Return an async function that accepts the same arguments as the original function (except the callback)
25 async def promisified_function(*args: Any) -> Any:
26 """
27 The promisified version of the original callback-based function
28
29 Args:
30 *args: Variable number of arguments passed to the function
31
32 Returns:
33 A Future that resolves with data or rejects with an error
34 """
35
36 # Get the current event loop
37 loop = asyncio.get_event_loop()
38
39 # Create a Future to wrap the callback-based function
40 future = loop.create_future()
41
42 # Define the callback handler that will be passed to the original function
43 def callback_handler(data: Any, error: Optional[str] = None) -> None:
44 """
45 Callback handler that bridges between callback style and Future style
46
47 Args:
48 data: The data to resolve with
49 error: Optional error message to reject with
50 """
51 # Check if an error was passed to the callback
52 if error:
53 # Reject the Future with the error
54 future.set_exception(Exception(error))
55 else:
56 # Resolve the Future with the data
57 future.set_result(data)
58
59 # Call the original function with the custom callback handler
60 # Spread the arguments that were passed to the promisified function
61 fn(callback_handler, *args)
62
63 # Return the Future that will be resolved/rejected by the callback
64 return await future
65
66 # Return the promisified function
67 return promisified_function
68
69
70# Example usage:
71# def example_callback(callback, value):
72# callback(value * 2, None)
73#
74# async_func = promisify(example_callback)
75# result = await async_func(21) # Returns 42
76
1import java.util.concurrent.CompletableFuture;
2import java.util.function.BiConsumer;
3
4/**
5 * Functional interface for a callback function that accepts a next callback and variable number of arguments
6 */
7@FunctionalInterface
8interface CallbackFn {
9 /**
10 * Executes the callback function
11 * @param next - BiConsumer callback that receives data and optional error
12 * @param args - Variable number of numeric arguments passed to the function
13 */
14 void execute(BiConsumer<Integer, String> next, int... args);
15}
16
17/**
18 * Functional interface for the promisified version of the callback function
19 */
20@FunctionalInterface
21interface Promisified {
22 /**
23 * Executes the promisified function
24 * @param args - Variable number of numeric arguments
25 * @return CompletableFuture that completes with an Integer or exceptionally with an error
26 */
27 CompletableFuture<Integer> execute(int... args);
28}
29
30/**
31 * Utility class for converting callback-based functions to Promise-based functions
32 */
33public class PromisifyUtil {
34
35 /**
36 * Converts a callback-based function into a Promise-based function
37 * @param fn - The callback-based function to be promisified
38 * @return A new function that returns a CompletableFuture instead of using callbacks
39 */
40 public static Promisified promisify(CallbackFn fn) {
41 // Return a lambda function that accepts the same arguments as the original function (except the callback)
42 return (int... args) -> {
43 // Create and return a new CompletableFuture that wraps the callback-based function
44 CompletableFuture<Integer> future = new CompletableFuture<>();
45
46 // Call the original function with a custom callback handler
47 fn.execute(
48 // Define the BiConsumer callback handler that will be passed to the original function
49 (Integer data, String error) -> {
50 // Check if an error was passed to the callback
51 if (error != null && !error.isEmpty()) {
52 // Complete the future exceptionally with the error
53 future.completeExceptionally(new RuntimeException(error));
54 } else {
55 // Complete the future successfully with the data
56 future.complete(data);
57 }
58 },
59 // Pass the arguments that were provided to the promisified function
60 args
61 );
62
63 return future;
64 };
65 }
66
67 /**
68 * Example usage:
69 * CallbackFn callbackFunc = (next, args) -> next.accept(42, null);
70 * Promisified asyncFunc = promisify(callbackFunc);
71 * asyncFunc.execute().thenAccept(System.out::println); // 42
72 */
73}
74
1#include <functional>
2#include <future>
3#include <string>
4#include <utility>
5
6/**
7 * Type definition for a callback function that accepts a next callback and variable number arguments
8 * The callback receives data (int) and optional error (string)
9 */
10template<typename... Args>
11using CallbackFn = std::function<void(std::function<void(int, const std::string&)>, Args...)>;
12
13/**
14 * Type definition for the promisified version of the callback function
15 * Returns a future that resolves with an int or throws an exception with error
16 */
17template<typename... Args>
18using Promisified = std::function<std::future<int>(Args...)>;
19
20/**
21 * Converts a callback-based function into a Promise-based function
22 * @param fn - The callback-based function to be promisified
23 * @returns A new function that returns a future instead of using callbacks
24 */
25template<typename... Args>
26Promisified<Args...> promisify(CallbackFn<Args...> fn) {
27 // Return a function that accepts the same arguments as the original function (except the callback)
28 return [fn](Args... args) -> std::future<int> {
29 // Create and return a promise/future pair that wraps the callback-based function
30 auto promise = std::make_shared<std::promise<int>>();
31 std::future<int> future = promise->get_future();
32
33 // Execute the original function asynchronously
34 std::thread([fn, promise, args...]() {
35 try {
36 // Call the original function with a custom callback handler
37 fn(
38 // Define the callback handler that will be passed to the original function
39 [promise](int data, const std::string& error) {
40 // Check if an error was passed to the callback
41 if (!error.empty()) {
42 // Set exception in the promise with the error
43 promise->set_exception(std::make_exception_ptr(std::runtime_error(error)));
44 } else {
45 // Set the value in the promise with the data
46 promise->set_value(data);
47 }
48 },
49 // Forward the arguments that were passed to the promisified function
50 args...
51 );
52 } catch (...) {
53 // Handle any exceptions thrown during execution
54 promise->set_exception(std::current_exception());
55 }
56 }).detach();
57
58 return future;
59 };
60}
61
62/**
63 * Example usage:
64 * auto callback = [](std::function<void(int, const std::string&)> next) { next(42, ""); };
65 * auto asyncFunc = promisify<>(CallbackFn<>(callback));
66 * auto future = asyncFunc();
67 * std::cout << future.get() << std::endl; // 42
68 */
69
1/**
2 * Type definition for a callback function that accepts a next callback and variable number arguments
3 * @param next - Callback function that receives data and optional error
4 * @param args - Variable number of numeric arguments passed to the function
5 */
6type CallbackFn = (next: (data: number, error: string) => void, ...args: number[]) => void;
7
8/**
9 * Type definition for the promisified version of the callback function
10 * @returns Promise that resolves with a number or rejects with an error
11 */
12type Promisified = (...args: number[]) => Promise<number>;
13
14/**
15 * Converts a callback-based function into a Promise-based function
16 * @param fn - The callback-based function to be promisified
17 * @returns A new function that returns a Promise instead of using callbacks
18 */
19function promisify(fn: CallbackFn): Promisified {
20 // Return an async function that accepts the same arguments as the original function (except the callback)
21 return async function (...args: number[]): Promise<number> {
22 // Create and return a new Promise that wraps the callback-based function
23 return new Promise<number>((resolve, reject) => {
24 // Call the original function with a custom callback handler
25 fn(
26 // Define the callback handler that will be passed to the original function
27 (data: number, error: string) => {
28 // Check if an error was passed to the callback
29 if (error) {
30 // Reject the Promise with the error
31 reject(error);
32 } else {
33 // Resolve the Promise with the data
34 resolve(data);
35 }
36 },
37 // Spread the arguments that were passed to the promisified function
38 ...args
39 );
40 });
41 };
42}
43
44/**
45 * Example usage:
46 * const asyncFunc = promisify(callback => callback(42));
47 * asyncFunc().then(console.log); // 42
48 */
49
Time and Space Complexity
Time Complexity: O(1)
for the promisify
function itself, and O(T)
for the execution of the returned async function, where T
is the time complexity of the original callback function fn
.
The promisify
function simply returns a new function wrapper without performing any iterations or recursive operations, making it a constant time operation. When the returned async function is invoked, it creates a Promise and calls the original function fn
once. The time complexity depends entirely on what the original callback function does internally.
Space Complexity: O(1)
for the promisify
function, and O(S + A)
for the returned async function, where S
is the space used by the original callback function fn
and A
is the space needed to store the args
array.
The promisify
function itself only creates and returns a function reference, using constant space. When the returned async function executes, it allocates space for:
- The Promise object and its internal state
- The closure that captures the
resolve
andreject
functions - The spread
args
array (which hasO(A)
space whereA
is the number of arguments) - Any additional space required by the original callback function
fn
The space complexity is dominated by the arguments array and whatever space the original callback function requires for its execution.
Common Pitfalls
1. Incorrect Callback Parameter Order
One of the most common mistakes is misunderstanding the callback parameter order. Many developers assume the error comes first (Node.js convention), but this problem specifically states that the callback receives (data, error)
- data first, error second.
Incorrect Implementation:
def callback_handler(error: Optional[str], data: Any) -> None: # Wrong order!
if error:
future.set_exception(Exception(error))
else:
future.set_result(data)
Solution:
Always verify the callback signature in the problem statement. In this case, ensure your callback handler accepts (data, error)
in that specific order.
2. Not Handling Synchronous Exceptions
The current implementation doesn't catch exceptions that might be thrown synchronously by the original function before it even calls the callback.
Problem Scenario:
def buggy_function(callback, value):
if value < 0:
raise ValueError("Value must be positive") # Synchronous exception!
callback(value * 2, None)
Solution: Wrap the function call in a try-except block:
async def promisified_function(*args: Any) -> Any:
loop = asyncio.get_event_loop()
future = loop.create_future()
def callback_handler(data: Any, error: Optional[str] = None) -> None:
if error:
future.set_exception(Exception(error))
else:
future.set_result(data)
try:
fn(callback_handler, *args)
except Exception as e:
future.set_exception(e)
return await future
3. Callback Called Multiple Times
Some callback-based functions might incorrectly call the callback multiple times. The Future/Promise can only be resolved once, and subsequent calls will cause errors.
Problem Example:
def flaky_function(callback, value):
callback(value, None)
callback(value * 2, None) # Second call - will cause issues!
Solution: Add a guard to ensure the Future is only set once:
def callback_handler(data: Any, error: Optional[str] = None) -> None:
if future.done(): # Check if already resolved
return
if error:
future.set_exception(Exception(error))
else:
future.set_result(data)
4. Memory Leaks with Never-Calling Callbacks
If the original function never calls the callback (due to a bug or design), the Future will never resolve, potentially causing memory leaks or hanging await statements.
Solution: Add a timeout mechanism:
async def promisified_function(*args: Any) -> Any:
loop = asyncio.get_event_loop()
future = loop.create_future()
def callback_handler(data: Any, error: Optional[str] = None) -> None:
if error:
future.set_exception(Exception(error))
else:
future.set_result(data)
fn(callback_handler, *args)
# Add timeout protection (adjust timeout as needed)
try:
return await asyncio.wait_for(future, timeout=30.0)
except asyncio.TimeoutError:
raise TimeoutError("Callback was never invoked")
In a binary min heap, the maximum element can be found in:
Recommended Readings
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Runtime Overview When learning about algorithms and data structures you'll frequently encounter the term time complexity This concept is fundamental in computer science and offers insights into how long an algorithm takes to complete given a certain input size What is Time Complexity Time complexity represents the amount of time
Want a Structured Path to Master System Design Too? Don’t Miss This!