Skip to content

Instantly share code, notes, and snippets.

@andrewsantarin
Last active September 8, 2023 09:41
Show Gist options
  • Save andrewsantarin/5f7638e6c8d2f0470ab0d13e3fc3b2f9 to your computer and use it in GitHub Desktop.
Save andrewsantarin/5f7638e6c8d2f0470ab0d13e3fc3b2f9 to your computer and use it in GitHub Desktop.
Identifying successful race conditions in Promises

race.ts

Wrapper over Promise.race([...promises]) to identify which condition was met.

Method signature interface is identical to Redux Saga's race({...promiseMap}) method, but without Redux Saga as a requirement. Straight up plain JavaScript async...await without dependencies. In TypeScript, return type is also identified according to object key provided per Promise instance (in Redux Saga, a key is described as a "label"), so, there is clear indication of which key maps to which expected type of return value.

Method signature:

race({
    [key: string]: Promise<any>
}) => Promise<{
    [Key in keyof PromiseRecord]?: Awaited<PromiseRecord[Key]>;
}>;
  • Each key you add is a label to a promise (Promise<any>).
  • An object, if successful, the key ("label") and the result of the associated promise that completed first.
  • An error, if unsuccessful, from the promise which has thrown it. Ideally, the error should be an Error instance.

Why?

Like the overview says, you want to know which one of the race conditions was met first, in plain JavaScript. The Promise.race() method does indicate that one of the conditions wins the race, but it doesn't tell you which one it was.

With proper support for the modern JavaScript syntax, there is simply no need to use a more powerful library such as Redux Saga. If your project is not sophisticated enough to demand the rest of Redux Saga's features, then all you would need are simple method wrappers over the JavaScript API, such as this race() method.

Examples

Successfully executed

const promise1 = new Promise<'one'>((resolve, reject) => {
    setTimeout(resolve, 500, 'one');
});

const promise2 = new Promise<'two'>((resolve, reject) => {
    setTimeout(resolve, 100, 'two');
});

const promise3 = new Promise<'three'>((resolve, reject) => {
    setTimeout(resolve, 1000, 'three');
});

const promise4 = new Promise<'four'>((resolve, reject) => {
    setTimeout(resolve, 5000, 'four');
});

async function main() {
    const { p1, p2, p3, p4 } = await race({
        p1: promise1,
        p2: promise2,
        p3: promise3,
        p4: promise4,
    });

    if (p1) {
        console.log('p1', p1);
    } else if (p2) {
        console.log('p2', p2);
    } else if (p3) {
        console.log('p3', p3);
    } else if (p4) {
        console.log('p4', p4);
    }
}

main();

Output in console:

p2 two

Unsuccessfully executed, but caught

Note: In Redux Saga, when an error is caught and is the first to complete the race(), the condition that caused this error is not identified. It is the reponsibility of the implementation detail to properly identify where exactly that error occurred.

const promise1 = new Promise<'one'>((resolve, reject) => {
    setTimeout(resolve, 500, 'one');
});

const promise2 = new Promise<'two'>((resolve, reject) => {
    setTimeout(resolve, 100, 'two');
});

const promise3 = new Promise<'three'>((resolve, reject) => {
    setTimeout(resolve, 1000, 'three');
});

const promise4 = new Promise<'four'>((resolve, reject) => {
    reject(new Error('p4 hit an error'));
});

async function main() {
    try {
        const { p1, p2, p3, p4 } = await race({
            p1: promise1,
            p2: promise2,
            p3: promise3,
            p4: promise4,
        });

        if (p1) {
            console.log('p1', p1);
        } else if (p2) {
            console.log('p2', p2);
        } else if (p3) {
            console.log('p3', p3);
        } else if (p4) {
            console.log('p4', p4);
        } else {
            console.log('???', { p1, p2, p3, p4 });
        }
    } catch (error) {
        console.log('error', error);
    }
}

main();

Output in console:

error Error: p4 hit an error
    at eval (eval at <anonymous> (runtime.ts:153:7), <anonymous>:29:12)
    at new Promise (<anonymous>)
    at eval (eval at <anonymous> (runtime.ts:153:7), <anonymous>:28:18)
    at runtime.ts:153:7

References

const race = <
PromiseRecord extends { [key: string]: Promise<any> }
>(
promiseRecord: PromiseRecord
) => {
const promises = Object.keys(promiseRecord).map((key) => {
const promise = promiseRecord[key];
return new Promise((resolve, reject) => {
promise
.then((value) => {
// Instead of returning the value directly, wrap it in an object.
// The object reveals which key owns the promise's return value.
resolve({
[key]: value,
});
})
.catch((error) => {
// Don't rewrap caught error before bubbling it to parent caller.
// Simply pass it upwards.
reject(error);
});
});
});
return Promise.race(promises) as Promise<{
[Key in keyof PromiseRecord]?: Awaited<PromiseRecord[Key]>;
}>;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment