Building a Minimal Reproduction
Let's scaffold a compact Angular application to examine the issue:
import 'zone.js/dist/zone';
import { Component } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
const users: Record<string, () => string> = {
corbin: () => 'Hello, world!',
};
@Component({
selector: 'welcome-msg',
standalone: true,
template: `
<p>Corbin says: {{welcomeMessage}}</p>
`,
})
export class WelcomeComponent {
// 🤫
welcomeMessage = users.crutchcorn();
}
@Component({
selector: 'my-app',
standalone: true,
imports: [WelcomeComponent],
template: `
<h1>The welcome app!</h1>
<welcome-msg/>
<p>That's all!</p>
`,
})
export class App {}
bootstrapApplication(App);
Upon rendering this application, the DOM output looks like this:
<my-app>
<h1>The welcome app!</h1>
<welcome-msg></welcome-msg>
</my-app>
Hold on—where has our
pelement gone? Where did both of thepelements go?!
Surprisingly, this is precisely what Angular is designed to do. It stems from an error in our code that is subtle yet crucial to understand.
Throughout this discussion, we'll cover:
- The cause behind this behavior
- The reasoning for its occurrence
- The corrective measures for our code
- Potential long-term improvements within Angular itself
Examining the Trigger
In the code sample above, the perceptive among you may have spotted a mistake with welcomeMessage. The error was such that we invoked a nonexistent method:
const users: Record<string, () => string> = {
corbin: () => 'Hello, world!',
};
// ...
welcomeMessage = users.crutchcorn();
Although this error might have been prevented by changing our users type as shown:
const users = {
corbin: () => 'Hello, world!',
} satisfies Record<string, () => string>;
It's not certain that similar errors won't occur again elsewhere.
Typos and other defects exist in codebases of every size, and although various precautions can be taken, eliminating them completely is unattainable.
This specific error is a TypeError: crutchcorn is not a function.
Let's swap this out for a throw statement to observe more clearly what occurs:
import 'zone.js/dist/zone';
import { bootstrapApplication } from '@angular/platform-browser';
import { Component } from '@angular/core';
@Component({
selector: 'throw-an-error',
standalone: true,
template: `<p>🙈</p>`,
})
class ErrorComponent {
constructor() {
throw 'This is an error';
}
}
@Component({
selector: 'my-app',
standalone: true,
imports: [ErrorComponent],
template: `
<p>Before</p>
<!-- Try hiding and showing this line -->
<throw-an-error/>
<!-- This never shows up -->
<p>After</p>
`,
})
class AppComponent {}
bootstrapApplication(AppComponent);
In this case, you'll observe that <p>Before</p> does render, but neither <p>🙈</p> nor <p>After</p> do, matching the earlier example.
However, if you relocate the <p>Before</p> tag to appear after the <throw-an-error/> component, as below:
<!-- Try hiding and showing this line -->
<throw-an-error/>
<!-- This never shows up -->
<p>Before</p>
<p>After</p>
Then neither Before nor After renders any longer. What explains this?
Understanding the Root Cause
If we pause for a moment and examine how Angular's compiler operates, we see that Angular takes a component template like this:
@Component({
selector: 'app-cmp',
template: '<span>Your name is {{name}}</span>',
})
export class AppCmp {
name = 'Alex';
}
And compiles it into a template function:
import { Component } from '@angular/core';
import * as i0 from "@angular/core";
export class AppCmp {
constructor() {
this.name = 'Alex';
}
}
AppCmp.ɵfac = function AppCmp_Factory(t) { return new (t || AppCmp)(); };
AppCmp.ɵcmp = i0.ɵɵdefineComponent({
type: AppCmp,
selectors: [["app-cmp"]],
decls: 2,
vars: 1,
template: function AppCmp_Template(rf, ctx) {
if (rf & 1) {
i0.ɵɵelementStart(0, "span");
i0.ɵɵtext(1);
i0.ɵɵelementEnd();
}
if (rf & 2) {
i0.ɵɵadvance(1);
i0.ɵɵtextInterpolate1("Your name is ", ctx.name, "");
}
},
encapsulation: 2
});
(function () { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(AppCmp, [{
type: Component,
args: [{
selector: 'app-cmp',
template: '<span>Your name is {{name}}</span>',
}]
}], null, null); })();
This code is taken from the Angular Blog on how the Angular compiler works.
Let's apply this understanding of the compiler to our own code. This implies that some code like the following:
<p>Render before</p>
<component/>
<p>Render after</p>
Will be compiled by Angular to approximately the following output:
renderBefore();
renderComponent()
renderAfter();
Although this functions, there's a catch: any errors thrown during the class constructor instantiation of renderComponent will interrupt the execution of the subsequent lines.
What's the reason?
JavaScript Error Handling
Reflecting on how errors are managed in JavaScript, a thrown error operates as an immediate, forced exit for a function, functioning much like a return statement:
function sayHi() {
throw "This is an error";
// This will never execute
console.log("Hello!");
}
This principle holds consistently, even when functions are nested within other functions:
function sayHi() {
throw "This is an error";
// This will never execute
console.log("Hello!");
}
function greet(name) {
sayHi();
console.log("My name is", name);
}
function greetWithName() {
greet("Corbin");
}
// Will never `console.log`, instead will throw an error from `sayHi`
greetWithName();
The same pattern applies within Angular, and there's no way to stop or alter it — it's inherent to JavaScript's fundamental nature.
A note on error catching
It's important to point out, though, that you can stop these errors from preventing subsequent code from executing by employing a try/catch block to halt the error's spread:
function sayHi() {
throw "This is an error";
// This will still never execute
console.log("Hello!");
}
function greet(name) {
try {
sayHi();
} catch (e) {
// `e` is the thrown error
// Log `e` to your error service, or do whatever you'd like to it
// ...
}
// This code now continues as if nothing ever happened, rather than early returning
console.log("My name is", name);
}
function greetWithName() {
greet("Corbin");
}
// This will now log "My name is Corbin", without the "Hello!"
greetWithName();
How JavaScript's Error Handling Impacts Angular
Returning to the Angular context, let's revisit the render pseudo-code from earlier:
renderBefore();
renderComponent()
renderAfter();
Suppose the renderComponent function causes an exception to be thrown during its constructor, just like the throw-an-error component from our previous example.
Let's pause and consider what's happening: when component is rendered, it throws an exception. The code can now be understood as analogous to this:
renderBefore();
throw new Error();
renderAfter();
Consequently, since thrown errors act as an immediate exit, the execution flow within the render template never gets a chance to reach the renderAfter section.
This explains why we observe parts of the template rendered before our throw-an-error component, while the sections after it remain missing.
Developers who have spent time in the Angular ecosystem are likely familiar with the Angular ErrorHandler API, which serves as a tool for logging and monitoring errors that are thrown within an application.
This API proves invaluable when you require a centralized approach to sending error reports to external services such as Sentry.
This raises the question: why is resolving this problem necessary in the first place?
Let's examine an edge case: a header component appears on every page of your application. This header somehow needs to request data from an API to display certain information. Perhaps you want to display metadata about the user's profile in the header.
But things take a turn! The API that your header depended on has changed unexpectedly, causing the header to throw an error during its construction process.
Due to this error propagation, instead of just a single segment of your application failing to display, every single page that includes the header is now rendered incorrectly.
This could encompass a critical business process that generates revenue — and the time to resolve this is dictated by the duration it takes your developers to implement a fix and redeploy.
No matter how improbable this situation appears; downtime often translates directly to financial loss. Although preventing errors at build time is the most effective strategy, it's certainly not the only point where robust error handling is required.
Other ecosystems' fix to this problem
Although this appears to be a challenge specific to Angular, it's not unique — several other frameworks have tackled the identical scenario of "an exception during rendering causing a corrupted user interface."
React provides a template-driven error handling solution in the form of an ErrorBoundary:
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// Update state so the next render will show the fallback UI.
return { hasError: true };
}
componentDidCatch(error, info) {
// Example "componentStack":
// in ComponentThatThrows (created by App)
// in ErrorBoundary (created by App)
// in div (created by App)
// in App
logErrorToMyService(error, info.componentStack);
}
render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return this.props.fallback;
}
return this.props.children;
}
}
// ...
<ErrorBoundary fallback={<p>Something went wrong</p>}>
<Profile />
</ErrorBoundary>
Fortunately, although a component like <component-here/> cannot automatically recover from an error, we can take manual control. By leveraging Angular's ViewContainerRef API, we can directly wrap our internal createComponent invocation in a try/catch structure:
import 'zone.js/dist/zone';
import { NgIf } from '@angular/common';
import { bootstrapApplication } from '@angular/platform-browser';
import {
Component,
inject,
OnInit,
Input,
ViewChild,
TemplateRef,
ViewContainerRef,
} from '@angular/core';
@Component({
selector: 'throw-an-error',
standalone: true,
template: `<p>🙈</p>`,
})
class ErrorComponent {
constructor() {
throw 'This is an error';
}
}
@Component({
selector: 'error-catcher',
standalone: true,
imports: [NgIf],
template: `
<div *ngIf="error">
<h1>There was an error</h1>
</div>
<ng-template #compTemp></ng-template>
`,
})
class ErrorCatcher implements OnInit {
@ViewChild('compTemp') compTemp!: TemplateRef<any>;
@Input({ required: true }) comp!: any;
containerRef = inject(ViewContainerRef);
error: any = null;
ngOnInit() {
try {
this.containerRef.createComponent(this.comp);
} catch (e) {
this.error = e;
}
}
}
@Component({
selector: 'my-app',
standalone: true,
imports: [ErrorCatcher],
template: `
<p>Before</p>
<error-catcher [comp]="comp"/>
<p>After</p>
`,
})
class AppComponent {
comp = ErrorComponent;
}
bootstrapApplication(AppComponent);
The level of control can be extended by enhancing the error-catcher component to also manage both incoming and outgoing data via inputs and outputs:
import { ErrorBoundary } from './error-boundary.component';
@Component({
selector: 'child',
standalone: true,
template: `<button (click)="done.emit()">Child: {{name}}, {{age}}</button>`,
})
class ChildComponent {
@Input() name!: string;
@Input() age!: number;
@Output() done = new EventEmitter();
}
@Component({
selector: 'error',
standalone: true,
template: `<p>Error</p>`,
})
class ErrorComponent {
constructor() {
throw 'Failed to construct ErrorComponent';
}
}
@Component({
selector: 'my-app',
standalone: true,
imports: [ErrorBoundary, ChildComponent, JsonPipe],
template: `
<p>Parent</p>
<hr/>
<error-boundary [fallback]="fallback" [comp]="errorComponent"/>
<ng-template #fallback let-error>
<h1>There was an error in <code>errorComponent</code></h1>
<pre><code>{{error | json}}</code></pre>
</ng-template>
<hr/>
<error-boundary [comp]="childComponent" (event)="getEvent($event)" [inputs]="{age, name: 'Janie'}"/>
<button (click)="count()">Count</button>
`,
})
class AppComponent {
childComponent = ChildComponent;
errorComponent = ErrorComponent;
age = 12;
count() {
this.age++;
}
getEvent(props: { name: string; value: unknown }) {
console.log({ props });
}
}
Why the quick workaround falls short
The immediate solution introduces several complications that are hard to ignore:
- Its syntax and tooling support diverge from what you'd expect when working with regular Angular components
- Both inputs and outputs end up with very loose type definitions, which can easily introduce more errors rather than preventing them
- Output binding works differently, forcing awkward
switch/casepatterns and manual type assertions
If these drawbacks are as unacceptable to you as they are to me, it's worth examining what a more permanent solution—one that's baked into Angular itself—could offer.
For those keeping an eye on Angular's developer experience evolution, the Angular team has been working on integrating New Control Flow primitives directly into the framework. These new primitives are designed to:
- Reduce the overall size of Angular's core bundle
- Streamline the process of introducing new core features
- Work seamlessly with Angular's emerging signals API
Here's a glimpse of what they might look like in practice:
@if (user.isHuman) {
<human-profile [data]="user" />
} @else if (user.isRobot) {
<robot-profile [data]="user" />
} @else {
<p>The profile is unknown!</p>
}
This block of code is the direct counterpart to the following:
<human-profile *ngIf="user.isHuman; else elseOne" [data]="user" />
<ng-template #elseOne>
<robot-profile *ngIf="user.isRobot; else elseTwo" [data]="user" />
<ng-template #elseTwo>
<p>The profile is unknown!</p>
</ng-template>
</ng-template>
My suggestion for the durable fix is to introduce a new @try/@catch syntax as part of Angular's core Control Flow primitives:
@try {
<error-throwing-component/>
} @catch (e: any) {
<handle-error [error]="e"/>
}
To push this proposal forward, I've filed a GitHub issue detailing the approach and have offered to implement it myself.
I'd really appreciate it if you could add a thumbs-up reaction to the GitHub issue to help it gain visibility with the Angular team.
