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

fix(router): handle constructor errors during initial navigation #49953

Closed
wants to merge 1 commit into from

Conversation

alan-agius4
Copy link
Contributor

@alan-agius4 alan-agius4 commented Apr 20, 2023

Currently, when there is an error inside a lazy loaded component constructor an unhandled promise rejection is thrown which causes the error handler not to be invoked on the server due to an issue with Zone.js see #49930.

While this is a workaround for that in general we should not cause unhandled promise rejections and always await and catch promises especially going for going zoneless, this is because Node.js does not wait for pending unhandled promises and these will cause the process to exit. See: nodejs/node#22088

Eventually, the framework will need to be updated to better handle promises in certain listeners for zoneless and avoid unhandled promises

Example on potential problematic code.

{
    provide: APP_BOOTSTRAP_LISTENER,
    useFactory: () => {
      Promise.resolve().then(() => {})
    },
}

This change is also a step closer to address #33642

@alan-agius4 alan-agius4 marked this pull request as draft April 20, 2023 12:48
@pullapprove pullapprove bot requested review from atscott and dylhunn April 20, 2023 12:48
@alan-agius4 alan-agius4 force-pushed the router-unahandled branch 2 times, most recently from d3ff4d6 to 85a3194 Compare April 20, 2023 13:01
@ngbot ngbot bot added this to the Backlog milestone Apr 20, 2023
@alan-agius4 alan-agius4 added the target: rc This PR is targeted for the next release-candidate label Apr 20, 2023
Currently, when there is an error inside a lazy loaded component constructor an unhandled promise rejection is thrown which causes the error handler not to be invoked on the server due to an issue with Zone.js see angular#49930.

While this is a workaround for that in general we should not cause unhandled promise rejections and always await and catch promises especially going for going zoneless, this is because Node.js does not wait for pending unhandled promises and these will cause the process to exit. See: nodejs/node#22088

Eventually, the framework will need to be updated to better handle  promises in certain listeners for zoneless and avoid unhandled promises

Example on potential problematic code.
```ts
{
    provide: APP_BOOTSTRAP_LISTENER,
    useFactory: () => {
      Promise.resolve().then(() => {})
    },
}
```

This change is also a step closer to address angular#33642
@alan-agius4 alan-agius4 marked this pull request as ready for review April 20, 2023 14:02
@alan-agius4
Copy link
Contributor Author

alan-agius4 commented Apr 20, 2023

caretaker: G3 failures are unrelated unrelated.

@alan-agius4 alan-agius4 added the action: review The PR is still awaiting reviews from at least one requested reviewer label Apr 20, 2023
router.initialNavigation();
const errorHandler = injector.get(ErrorHandler, null);
const initialNavigation = router.initialNavigation();
if (errorHandler) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error would already be surfaced through the router’s errorHandler as well as the events as a NavigationError, right? Do we really need it raised in a third place?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No the error is not captured in any other place at least only the unhandled promise rejection is currently surfaced to the user (console) when there is an error in the component ctor.

At the moment it is surfaced as an unhandled promise rejections, this just captures it and pass it to the error handler.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I stand corrected, the error is being added as navigation error. So the problem here is that the error handler provided from the DI is not used.

What are your thoughts about using the error handler provided through the DI token (ERROR_HANDLER) instead of an option to the router? Something like;

  private coreErrorHandler = inject(ERROR_HANDLER);

  /**
   * A handler for navigation errors in this NgModule.
   *
   * @deprecated Subscribe to the `Router` events and watch for `NavigationError` instead.
   *   `provideRouter` has the `withNavigationErrorHandler` feature to make this easier.
   * @see `withNavigationErrorHandler`
   */
  errorHandler = this.options.errorHandler ?? function defaultErrorHandler(error: unknown): never {
     this.coreErrorHandler.handle(error);
     throw error;
  }

This is important so not to make platform-server dependent on the router and have to listen to navigation errors from the router directly, where when using SSG we do want navigation errors to be fatal and causes the process to exit with a non zero error code.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I would actually be more in favor of the initialNavigation catching and swallowing the promise rejection since it’s surfaced elsewhere. That should resolve the issue as well, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem with catching the error will be swallowed since errors captured as NavigationErrors by default are not surfaced to the users. This is problematic as “critical” errors such as during component construction will not be surfaced unless users subscribe to the router events.

I see a couple of possible solutions.

  1. Catch the rejection and log it.
this.scheduleNavigation(urlTree, source, restoredState, extras).catch(e => this.errorHandler.handleError(e));
  1. Catch the rejection and change the defaultErrorHandler and capture log it
private coreErrorHandler = inject(ERROR_HANDLER);

errorHandler = this.options.errorHandler ?? function defaultErrorHandler(error: unknown): never {
   this.coreErrorHandler.handle(error);
   throw error;
}
...
this.scheduleNavigation(urlTree, source, restoredState, extras).catch(() => {});

The above somewhat aligns with what you want to do in #48910 but it does not remove the throw yet.

  1. Catch the error and register a default NavigationErrorHandler when none is provided.
export function provideRouter(routes: Routes, ...features: RouterFeatures[]): EnvironmentProviders {
  const providers: Provider[] = [
    {provide: ROUTES, multi: true, useValue: routes},
    typeof ngDevMode === 'undefined' || ngDevMode
      ? {provide: ROUTER_IS_PROVIDED, useValue: true}
      : [],
    {provide: ActivatedRoute, useFactory: rootRoute, deps: [Router]},
    {provide: APP_BOOTSTRAP_LISTENER, multi: true, useFactory: getBootstrapListener},
    features.map((feature) => feature.ɵproviders),
  ];

  if (!features.some(f => f.ɵkind === RouterFeatureKind.NavigationErrorHandlerFeature)) {
    providers.push(
      withNavigationErrorHandler((e) => inject(ErrorHandler).handleError(e)).ɵproviders
    );
  }

  return makeEnvironmentProviders(providers);
}
....
this.scheduleNavigation(urlTree, source, restoredState, extras).catch(() => {});

Copy link
Contributor

@atscott atscott left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to be honest, I’m not a big fan of the Promise rejection at all right now. The rejection is pretty much never handled, except in tests. When’s the last time you’ve seen a try…catch or promise.catch/finally with a router navigation?

Since I’d like to change this behavior, I’m worried about surfacing the Promise in yet another place where it needs to be handled

#48910

@alan-agius4 alan-agius4 deleted the router-unahandled branch April 20, 2023 17:38
@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators May 22, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
action: review The PR is still awaiting reviews from at least one requested reviewer area: router target: rc This PR is targeted for the next release-candidate
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants