Skip to content

Cannot finish a race with timer in RxJS

race(
  timer(2000).pipe(mapTo(1)),
  timer(1000).pipe(mapTo(2)),
).toPromise().then(r => console.log(r))

Code above will output 2.

If I have an action that I try to repeat until condition is met but ignore it if it takes too long, then I take an approach in this answer https://stackoverflow.com/a/51644077

The problem is that race never finishes with the shortest function. The longActionObservable repeats until the condition is met and empty() is called.

const o = longActionObservable();
race(
  o.pipe(expand(v => v < 100 ? empty() : o)), // no toArray because I do not need result
  timer(2000),
).toPromise()

This code does not return a promise that will ever resolve. I’m wondering why is this approach so fragile to this kind of behavior? If there’s one observable timer(2000) that most surely ends, why doesn’t race end?

Answer

You are missing an important point about race:

The observable to emit first is used

This means that if first emission of longActionObservable occurs before the timer, then timer is not used, regardless of how long the “expand” observable takes to complete.

no toArray because I do not need result

Even though you don’t need the result, toArray actually makes this work as you wish because it will not allow any emissions until your “expand” observable completes. Instead of using toArray, you could use reduce instead:

race(
  o.pipe(
    expand(v => v < 100 ? empty() : o), 
    reduce(() => undefined)
  ),
  timer(2000),
)
.toPromise()

If there’s one observable timer(2000) that most surely ends, why doesn’t race end?

The only reason race will not end is because the chosen source (first observable to emit) does not complete. Check the emissions of your expand observable to see why it isn’t completing:

  o.pipe(
    expand(v => v < 100 ? empty() : o),
    tap(v => console.log('expand: ', v)),
    reduce(() => undefined)
  ),

Here’s a StackBlitz you can play around with 🙂