Versions
Measured on:
| Package |
Version |
| @angular/build |
22.1.5 |
| @angular/cli |
22.1.5 |
| @angular/ssr |
22.1.5 |
| @angular/core |
22.1.3 |
| @angular/router |
22.1.3 |
| TypeScript |
6.0.3 |
| Node.js |
24.18.0 |
Summary
When a prerendered route fails to render, @angular/build writes no file for it, but still
counts it in Prerendered N static routes., still lists it in prerendered-routes.json, and
still exits 0.
The render error itself is printed. What is not reported is that a route the build claims to
have prerendered produced no output. The count and the manifest are derived from the list of
routes that were declared RenderMode.Prerender, never from the set that actually rendered.
Reproduction
An application with a route declared RenderMode.Prerender whose component throws during
activation. Minimal form:
@Component({ selector: 'app-second', standalone: true, template: '<p>second</p>' })
export class SecondRoute {
constructor() {
throw new Error('render failure');
}
}
// app.routes.ts
export const routes: Routes = [
{ path: '', component: HomeRoute },
{ path: 'second', component: SecondRoute },
];
// app.routes.server.ts
export const serverRoutes: ServerRoute[] = [
{ path: 'second', renderMode: RenderMode.Prerender },
{ path: '**', renderMode: RenderMode.Server },
];
ng build --configuration production
Expected
The build fails, or at minimum warns that a route declared for prerendering produced no output —
and prerendered-routes.json lists only routes that were written.
Actual
ERROR Error: render failure
at new SecondRoute (.../main.server.mjs:84:1005)
at ... activateRoutes (.../main.server.mjs:12:68228)
✔ Building...
Prerendered 4 static routes.
Application bundle generation complete.
Exit code 0.
In the application this was measured on, four routes were declared for prerendering across two
locales. With one component throwing:
- Files written to dist/<app>/browser: 2
- Routes listed in prerendered-routes.json: 4 — including both addresses that produced no file
- Reported: Prerendered 4 static routes.
- Exit code: 0
The same build with the throw removed writes 4 files and reports 4. So the reported number is
identical whether or not half the pages exist.
Mechanism
The render worker resolves with null rather than rejecting; a rejection would be reported and
would fail the build, so this path is specifically the silent one.
-
The component throws during route activation. Angular's ErrorHandler prints it, and the
navigation does not complete, so router.lastSuccessfulNavigation() has no finalUrl.
-
@angular/ssr's renderAngular leaves hasNavigationError at its initial true
(ssr.mjs:134, cleared only at :156/:158).
-
handleRendering returns null for that (ssr.mjs:1310-1311), and so does handle.
-
render-worker.js renderPage: if (!response) { return null; }.
-
prerender.js renderPages:
const renderResult = render
.then((content) => {
if (content !== null) {
output[outPath] = { content, appShellRoute: ... };
}
})
.catch((err) => {
errors.push(`An error occurred while prerendering route '${route}'...`);
});
content === null takes neither branch: no output entry, and nothing pushed to errors.
-
execute-post-bundle.js then builds the manifest from a different source — the declared
route tree rather than output:
for (const [path, { content, appShellRoute }] of Object.entries(output)) {
// ... writes the files that actually rendered
}
for (const metadata of serializableRouteTreeNode) {
serializableRouteTreeNodeForManifest.push(metadata);
if (metadata.renderMode === RouteRenderMode.Prerender && !metadata.route.includes('*')) {
prerenderedRoutes[metadata.route] = { headers: metadata.headers };
}
}
The two loops never meet, so prerenderedRoutes cannot notice that output is missing an entry.
Why it matters
The failure is quiet in the place it counts. CI that checks the exit code passes. A deployment
that trusts prerendered-routes.json — to prime a CDN, to assert coverage, to decide what to
upload — is told about pages that are not there. The render error scrolls past in a build log
that ends in a success line and a number that agrees with what was asked for.
It is worse for a localized application, where each locale is a separate prerendered address of
the same route. One locale failing to render leaves a hole at the addresses that locale's visitors
use, while the build reports the full count and every other locale is fine.
A one-line reconciliation between output and prerenderedRoutes — warn, or fail, on a declared
Prerender route with no output entry — would make it legible.
Versions
Measured on:
Summary
When a prerendered route fails to render, @angular/build writes no file for it, but still
counts it in Prerendered N static routes., still lists it in prerendered-routes.json, and
still exits 0.
The render error itself is printed. What is not reported is that a route the build claims to
have prerendered produced no output. The count and the manifest are derived from the list of
routes that were declared RenderMode.Prerender, never from the set that actually rendered.
Reproduction
An application with a route declared RenderMode.Prerender whose component throws during
activation. Minimal form:
Expected
The build fails, or at minimum warns that a route declared for prerendering produced no output —
and prerendered-routes.json lists only routes that were written.
Actual
ERROR Error: render failure at new SecondRoute (.../main.server.mjs:84:1005) at ... activateRoutes (.../main.server.mjs:12:68228) ✔ Building... Prerendered 4 static routes. Application bundle generation complete.Exit code 0.
In the application this was measured on, four routes were declared for prerendering across two
locales. With one component throwing:
The same build with the throw removed writes 4 files and reports 4. So the reported number is
identical whether or not half the pages exist.
Mechanism
The render worker resolves with null rather than rejecting; a rejection would be reported and
would fail the build, so this path is specifically the silent one.
The component throws during route activation. Angular's ErrorHandler prints it, and the
navigation does not complete, so router.lastSuccessfulNavigation() has no finalUrl.
@angular/ssr's renderAngular leaves hasNavigationError at its initial true
(ssr.mjs:134, cleared only at :156/:158).
handleRendering returns null for that (ssr.mjs:1310-1311), and so does handle.
render-worker.js renderPage: if (!response) { return null; }.
prerender.js renderPages:
content === null takes neither branch: no output entry, and nothing pushed to errors.
execute-post-bundle.js then builds the manifest from a different source — the declared
route tree rather than output:
The two loops never meet, so prerenderedRoutes cannot notice that output is missing an entry.
Why it matters
The failure is quiet in the place it counts. CI that checks the exit code passes. A deployment
that trusts prerendered-routes.json — to prime a CDN, to assert coverage, to decide what to
upload — is told about pages that are not there. The render error scrolls past in a build log
that ends in a success line and a number that agrees with what was asked for.
It is worse for a localized application, where each locale is a separate prerendered address of
the same route. One locale failing to render leaves a hole at the addresses that locale's visitors
use, while the build reports the full count and every other locale is fine.
A one-line reconciliation between output and prerenderedRoutes — warn, or fail, on a declared
Prerender route with no output entry — would make it legible.