6

I am having trouble understanding the finalize operator in RxJS. Let me demonstrate this on an example:

of(null).pipe(
  tap({ complete: () => console.log('tap 1 completes') }),
  finalize(() => console.log('finalize')),
  tap({ complete: () => console.log('tap 2 completes') })
).subscribe({ complete: () => console.log('subscribe completes') });

I would expect the finalize callback to be executed before the second tap. That's not happening, though. Rather the above code produces the following output:

tap 1 completes
tap 2 completes
subscribe completes
finalize

Looking at the implementation I believe the operator is passed (lifted) through the whole observable chain to always be applied at its end. So now I end up with two questions:

  1. What's the rationale behind this design decision? Can you give some explanation on why this is a desireable / advantageous property?
  2. Is there a different operator or other solution to execute code on complete and on error, but in order (i.e. before the second tap in the above example) rather than at the end of the observable chain?
Dennis Kozevnikoff
  • 2,078
  • 3
  • 19
  • 29
kremerd
  • 1,496
  • 15
  • 24

2 Answers2

8

It's important to be aware that finalize() and tap() work very differently. tap() is triggered by next, error and complete notifications while finalize() is only triggerd on chain ubsubscription. In other words finalize() is very similar to using:

const subscription = $source.subscribe();
// This will be always triggered after all `tap()`s
subscription.add(() => console.log('same as finalize()'));

So you can't make finalize() to be invoked before tap(). Also, be aware that finalize() is invoked also when you manually unsubscribe liek the following:

subscription.unsubscribe(); // will always invoke `finalize()` but never `tap()`

One possible solution could be implementing you own finalize() variant that knows the reason why it's being called: https://github.com/martinsik/rxjs-extra/blob/master/doc/finalizeWithReason.md (see source code)

Also note, that https://github.com/ReactiveX/rxjs/pull/5433 will have no affect on your use-case.

martin
  • 93,354
  • 25
  • 191
  • 226
  • Interesting! So far I thought of `finalize` as an RxJS equivalent of `Promise.finally`. Apparently there's much more going on under the hood here. – kremerd Jul 19 '20 at 12:52
4

That's the whole principle of the finalize operator. To emit only -after- the source observable completes. Which does mean, after all the complete subscriptions have been handled, counting the tap complete. From the docs:

Returns an Observable that mirrors the source Observable, but will call a specified function when the source terminates on complete or error.

Now you could place the finalize in an inner observable, but I suppose you wouldn't like the order then either

of(null).pipe(
  tap({ complete: () => console.log('tap 1 completes') }),
  concatMap((resp) => of(resp).pipe(
    finalize(() => console.log('finalize'))
  )),
  tap({ complete: () => console.log('tap 2 completes') })
).subscribe({ complete: () => console.log('subscribe completes') });

This will make the finalize execute before the first and last tap, this is because of the complete object you pass into tap. If you just pass in a function in the first tap, it will have the right order.

Another way could be the usage of concat:

concat(
  of(null).pipe(
    tap({complete: () => console.log('tap 1 completes' ) }),
    finalize(() => console.log('finalize'))
  ),
  EMPTY.pipe(
    tap({complete: () => console.log('tap 2 completes' ) })
  )
).subscribe({ complete: () => console.log('subscribe completes') });

But this kinda prevents you from accessing what ever the first observable emitted. So, basically, I don't think there is a proper solution to what you are asking :)

Poul Kruijt
  • 69,713
  • 12
  • 145
  • 149
  • Thanks for your suggestions! By now I think I'm actually searching for a variant of `tap`, rather than a variant of `finalize`. I guess I can just define that myself: `const tapOnEnd = callback => tap({ complete: () => callback(), error: () => callback() })`. – kremerd Jul 19 '20 at 13:06
  • @kremerd I'm kind of sure that the `finalize` will be called after that as well – Poul Kruijt Jul 19 '20 at 13:09
  • You're right. I mean to use it instead of the `finalize` operator, though, not instead of the other `tap`s. This way the execution order should be straight as it's just dealing with a sequence of `tap`s. – kremerd Jul 19 '20 at 13:42
  • I forgot about that, when an observable errors, it will call `complete` as well. If you want these functions to be the same, you should just use the `complete`. Check [here](https://stackblitz.com/edit/rxjs-kehls) (i've implemented your `tapOnEnd`) – Poul Kruijt Jul 19 '20 at 14:41
  • 1
    I don't think that `complete` is called on errors. I also tried it in a [Stackblitz](https://stackblitz.com/edit/rxjs-dlx587), and only the `error` callback is called. Maybe I'm missing something, though. That "here" link is broken, so I haven't been able to check your implementation. – kremerd Jul 20 '20 at 17:10
  • @kremerd I'm sorry, missed the last character. Here it [is](https://stackblitz.com/edit/rxjs-kehlsj) – Poul Kruijt Jul 20 '20 at 20:04