Skip to content

Feature Request: Forward rejected Promises from template event listeners to ErrorHandler #69228

@MillerSvt

Description

@MillerSvt

Which @angular/* package(s) are relevant/related to the feature request?

No response

Description

Angular template event listeners currently forward only synchronous exceptions to ErrorHandler.

For example:

@Component({...})
export class AppComponent {
  async onClick(): Promise<void> {
    throw new Error('Boom');
  }
}
<button (click)="onClick()">Click</button>

The rejection from onClick() is not forwarded to Angular's ErrorHandler. Only synchronous exceptions are handled.

Current implementation:

function executeListenerWithErrorHandling(
  lView,
  context,
  listenerFn,
  e,
) {
  const prevConsumer = setActiveConsumer(null);

  try {
    return listenerFn(e) !== false;
  } catch (error) {
    handleUncaughtError(lView, error);
    return false;
  } finally {
    setActiveConsumer(prevConsumer);
  }
}

Only synchronous exceptions are captured. If listenerFn returns a Promise, any later rejection escapes this error handling path.

Motivation

This becomes especially problematic in zoneless applications and Web Components.

A common recommendation is to rely on:

  • window.onunhandledrejection
  • provideBrowserGlobalErrorListeners()

However, this is not always acceptable.

For example, when Angular is used inside a Web Component, registering a global unhandledrejection listener causes the component to receive Promise rejections originating from unrelated code running on the host page. This makes error attribution difficult and may result in reporting errors that do not belong to the Angular application.

As a result, developers must manually wrap every async template listener:

onClick(): void {
  void this.load().catch(error => {
    this.errorHandler.handleError(error);
  });
}

or introduce custom helper abstractions for all event handlers.

Proposed solution

If a template listener returns a Promise (or PromiseLike), Angular could attach a rejection handler and forward the error to ErrorHandler.

Example:

const result = listenerFn(e);

if (isPromiseLike(result)) {
  result.catch(error => {
    handleUncaughtError(lView, error);
  });
}

return result !== false;

This would preserve current synchronous behavior while ensuring that asynchronous failures originating from Angular template listeners are reported consistently.

This would not catch intermediate floating Promises in compound template expressions, for example:

(click)="async1(); async2()"

In this case only the result of the last expression can be observed by the listener execution pipeline. This limitation seems acceptable and is consistent with normal JavaScript semantics. Developers would still need to avoid floating Promises inside compound expressions.

The proposed feature is only about handling the Promise returned by the template listener expression itself.

Expected benefit

Template event listeners would behave consistently regardless of whether they fail synchronously or asynchronously:

<button (click)="save()">Save</button>
save() {
  throw new Error(); // ErrorHandler
}

async save() {
  throw new Error(); // ErrorHandler
}

This would improve ergonomics in zoneless applications and embedded Angular Web Components while avoiding reliance on global unhandledrejection handlers.

Alternatives considered

Global unhandledrejection handling

Applications can register a global unhandledrejection listener or use provideBrowserGlobalErrorListeners().

However, this solution operates at the browser level rather than the Angular application level. In embedded scenarios (for example, Angular running inside a Web Component), the listener may receive Promise rejections originating from unrelated code on the host page.

As a result, applications cannot reliably distinguish between errors produced by Angular template listeners and errors produced by external code.

Manual error handling in every async listener

Developers can explicitly catch and forward errors:

onClick(): void {
  void this.save().catch(error => {
    this.errorHandler.handleError(error);
  });
}

This works but requires boilerplate for every async template listener and is easy to forget. It also creates an inconsistency between synchronous and asynchronous handlers, even though both are executed from Angular-managed template bindings.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: coreIssues related to the framework runtimegemini-triagedLabel noting that an issue has been triaged by gemini

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions