RxJS Mastery – #7 bindCallback & bindNodeCallback

In the early days of JavaScript we often used callbacks to handle the results of an asynchronous operation. If we now want to use those “old” functions in a more reactive way RxJS offers us a handy wrapper. With the two variants of bindCallback we can pass a function that formerly used a callback and we get back an Observable. This Observable delivers the values as notifications that would be passed in the callback function (as result or error).

RxJS bindCallback and bindNodeCallback operators explained 🎓

The function we pass to bindCallback is defined as follows in the RxJS implementation:

callbackFunc: (...args: [...any[], (...res: any) => void]) => void,

This means the function we are wrapping has the following characteristics:

  • can have an arbitrary number of arguments at the beginning of the parameters
  • the last parameter needs to be the callback function.

Let’s take the following multiplier function as example:

const multiplier = (input: number, input2: number, callback: (result: number) => void) => {
    setTimeout(() => {
        callback(input * input2);
    }, 1000);
};

const heavyTaskObservable = bindCallback(multiplicator);
const output$ = heavyTaskObservable(5, 3);
// if we subscribe to it we get 15 as next notification's value

Of course the code behind the multiplier function is only executed once we subscribe to the output$ observable. This is also illustrated by the following drawing:

The functionality behind the callback, e.g. the multiplication, runs after the subscription happened. When the (async) task is done the value is returned that would normally be passed as the parameter of the callback function.

bindNodeCallback

BindNodeCallback is similar to bindCallback but requires the callback function to be of the following form:

callback(error, result)

As you might have already guessed the error case is handled by the Observable emitting an error notification. Let’s modify the multiplier function a bit for demo purposes. Our calculator can now only handle results up to the number 1000 otherwise an error is passed to the callback:

it('should bind with a node callback too error case', (done) => {
    const multiplier = (input: number, input2: number, callback: (error: string | undefined, result: number) => void) => {
        setTimeout(() => {
            let result = input * input2;
            const error = result > 1000 ? 'numbers too large' : undefined;
            callback(error, result);
        }, 1000);
    };

    const heavyTaskObservable = bindNodeCallback(multiplier);
    const output$ = heavyTaskObservable(500, 3);

    output$.subscribe({
        error: (err) => {
            expect(err).toEqual('numbers too large');
            done();
        }
    })
});

What problems do the RxJS bindCallback operators solve? 🚧

Basically bindCallback and bindNodeCallback provide you the functionality to bring some reactivity to code that uses callbacks. This is especially helpful when dealing with third party code that you cannot change.

How to test the bindCallback operators🚦

Normally we have something behind the heavy operator that is not based on Observables. Otherwise we would probably not need to wrap it with bindCallback. Therefore we need to use the more traditional approach of testing asynchronous code and call done() as soon as the async operation is finished and the assertion is done.

it('should bind', (done) => {
    const heavyTask = (input: number, callback: (string) => void) => {
        setTimeout(() => {
            callback(`heavy task result for input ${input}`);
        }, 1000);
    };

    const heavyTaskObservable = bindCallback(heavyTask);
    const output$ = heavyTaskObservable(5);

    output$.subscribe({
        next: (val) => {
            expect(val).toEqual('heavy task result for input 5');
            done();
        }
    })
});

Often it is probably even better to mock the Observable returned by the bindCallback. Because we don’t need to test the implementation of bindCallback itself. In such cases rxjs marbles can be helpful. We should also always try to not use setTimeout in our mocked implementations with a value of more than a few milliseconds. Otherwise our test duration will be unnecessarily high.

Exercise for the bindCallback operators 💪

Take any third party JavaScript library you know. Search for a function that uses callbacks. Apply the bindCallback operator to have an Observable instead.