Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tapResponse swallowing errors happening inside the tapResponse #3431

Closed
1 of 2 tasks
maxime1992 opened this issue May 25, 2022 · 12 comments
Closed
1 of 2 tasks

tapResponse swallowing errors happening inside the tapResponse #3431

maxime1992 opened this issue May 25, 2022 · 12 comments

Comments

@maxime1992
Copy link
Contributor

Minimal reproduction of the bug/regression with instructions

    // not working as expected
    of(null)
      .pipe(
        tapResponse(
          () => {
            throw new Error(
              `This error will be swallowed and you'll have a hard time debugging`
            );
          },
          (error) => console.log('Some error happened', error)
        )
      )
      .subscribe();

That error ☝️ is silently caught which was painful to debug in my case. Here's a live repro on stackblitz

https://stackblitz.com/edit/angular-ivy-7jhxew?file=src%2Fapp%2Fapp.component.ts

Minimal reproduction of the bug/regression with instructions

To have an error thrown correctly.

Versions of NgRx, Angular, Node, affected browser(s) and operating system(s)

image

Other information

This very much looks like an issue I had with ngrxPush pipe here: #3100

I would be willing to submit a PR to fix this issue

  • Yes
  • No
@maxime1992 maxime1992 added the bug label May 25, 2022
@maxime1992
Copy link
Contributor Author

Not sure what's best here between

  • not handling an error that'd happen in the tapResponse next callback so that it's broadcasted all the way up
  • handle the error happening within the tapResponse next callback within the catch error of that custom operator

@markostanimirovic
Copy link
Member

@maxime1992

The tapResponse is used to keep the effect active if an error is thrown. Because of that, tapResponse swallows the thrown error. The error callback is not executed, because the error that is thrown within the next callback of tap operator will not be caught by its error callback:

**tap**({
  next() {
    throw new Error('ERROR!');
  },
  error(err) {
    // ERROR! won't be caught here
  }
})

The solution could be to move the error callback execution from tap to catchError:

function tapResponse(next, error, complete?) {
  return pipe(
    tap({ next, complete }),
    catchError((err) => {
      error(err);
      return EMPTY;
    });
  );
}

cc @alex-okrushko


For now, you can use tap + catchError:

.pipe(
  tap(() => {
    throw new Error('ERROR!');
  }),
  catchError((err) => {
    // handle error here

    return EMPTY;
   })
)

@timdeschryver
Copy link
Member

The solution could be to move the error callback execution from tap to catchError

I would find that weird. For me, or how I think about it, the error callback is to handle errors that are thrown while doing the request, not for errors in next.

What about adding a config, similar to the EffectConfig to bypass the default behavior?

@markostanimirovic
Copy link
Member

I would find that weird. For me, or how I think about it, the error callback is to handle errors that are thrown while doing the request, not for errors in next.

Yes 😐, it can be confusing because the behavior would be different from the tap operator.

What about adding a config, similar to the EffectConfig to bypass the default behavior?

🤔 I'd not add config argument, because the implementation with tap + catchError looks simpler IMO:

  • tap + catchError:
tap(() => {
  throw new Error('ERROR');
}),
catchError((err) => {
  // handle error
  return EMPTY;
}),
  • tapResponse + config:
tapResponse(
  () => {
    throw new Error('ERROR'),
  },
  (err) => {
    // handle error
  },
  { handleUnexpectedErrors: true },
)

@maxime1992
Copy link
Contributor Author

What about adding a config, similar to the EffectConfig to bypass the default behavior?

I don't think this would be good enough. Unless this is pretty much the first thing first first thing in the documentation, someone could miss it and the frustration to understand what's going on with a silently swallowed error can be really high. I do believe there's a need for a default behavior where you can handle this. I can't think of a case where someone would let its stream error without being warned at least

@spierala
Copy link

spierala commented Jun 15, 2022

Probably a super naive solution, still I want to share it:

Try catch around the nextFn...

export function tapResponse<T, E = unknown>(
    nextFn: (next: T) => void,
    errorFn: (error: E) => void,
    completeFn?: () => void
): (source: Observable<T>) => Observable<T> {
    return (source) => {
        return source.pipe(
            tap({
                next: (v) => {
                    try {
                        nextFn(v);
                    } catch (error) {
                        console.error('An error occurred in the next callback', error);
                    }
                },
                error: errorFn,
                complete: completeFn,
            }),
            catchError(() => EMPTY)
        );
    };
}

OK, looking at it again a few days later... I do not like it anymore. The error callback and complete callback could swallow errors as well. These callbacks would need try catch as well. That would become ugly quickly :)

FYI: The effect method example from here (https://ngrx.io/guide/component-store/effect#effect-method) would swallow the next fn error as well:
image

The example could be rewritten to this:

switchMap((id) => apiCall(id).pipe(
        tap({
          next: (movie) => this.addMovie(movie)
        }),
        catchError(() => {
          this.logError(e);
          return EMPTY;
      ))),

Then api errors and next errors would be logged.
I believe tapResponse should work like that.
Thats pretty much what @markostanimirovic suggested already.

@spierala
Copy link

spierala commented Jul 7, 2022

Fixed it with try catch in MiniRx :) https://github.com/spierala/mini-rx-store/pull/117/files

@alvipeo
Copy link

alvipeo commented Aug 11, 2022

Just spent hours finding this problem. Here's the example showing the problem - https://stackblitz.com/edit/angular-ivy-nxf2la.

@balaji-balamurugan
Copy link

Just spent hours finding this problem. Here's the example showing the problem - https://stackblitz.com/edit/angular-ivy-nxf2la.

what is the solution for this? any work around @markostanimirovic?

@alex-okrushko
Copy link
Member

Even through there is an expectation that next callback doesn't throw an error, we can adjust to catch errors in the next as well, I don't think there's a problem.

The question that I have is would you expect an error in the error callback to be handled somehow as well?

@alvipeo
Copy link

alvipeo commented Aug 14, 2022

In my opnion, whatever happens in both - no exception must be swallowed. If it throws in next => error should be fired. If it's thrown in error => console.error by default. But add the option to override this.

@markostanimirovic
Copy link
Member

Closed by #3533

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants