skip to content

Why does an Angular component that reads window.innerWidth in its constructor crash during server-side rendering, and what are the server-safe alternatives?

level: juniorimportance: must knowfreq 66%

answer

  1. same code runs in Node
  2. no window, no localStorage
  3. run DOM work after render
  4. inject, don't reach for globals

basics

~20 s

During SSR the component's constructor runs in Node, where window, localStorage and navigator do not exist, so the read throws. Move browser work into afterNextRender, reach the document through the DOCUMENT token, or guard browser-only logic with isPlatformBrowser(PLATFORM_ID).

solid answer

~40 s

With server-side rendering Angular runs the same component code on the server to produce HTML. That code runs in Node, which has no `window`, `localStorage`, `navigator` or `location`, so `window.innerWidth` in a constructor throws a `ReferenceError` and the render fails. The fixes, in order of preference: do DOM and layout work in `afterNextRender()`, which is skipped on the server and runs once in the browser after rendering; use `inject(DOCUMENT)` instead of the global `document`; for other browser-only logic, check `isPlatformBrowser(inject(PLATFORM_ID))`, or give the service separate server and browser implementations. The template should render the same content on both sides, and the browser-specific behaviour is applied after hydration.

go deeper

for a junior

Remember that component code runs in Node during SSR, where window and localStorage do not exist, and that afterNextRender is the default place for browser-only work.

for a middle

Explain which tool fits which job: afterNextRender for DOM work, DOCUMENT for document access, isPlatformBrowser for other branches, and why templates must match.

for a senior

Spot hidden global access in third-party imports and field initializers, and keep server and browser output identical to avoid hydration problems.

for a principal

Set a codebase rule for browser-only code, such as wrappers or platform providers, so SSR safety does not depend on every author remembering it.

## Why the same component runs twice With **server-side rendering (SSR)**, Angular bootstraps your application on the server for each request (or at build time for prerendered routes), runs constructors, lifecycle hooks and change detection, and serializes the resulting DOM into HTML. The browser then boots the same application and **hydrates** that HTML. Every constructor, field initializer and `ngOnInit` therefore runs **twice**: once in Node and once in the browser. Node is not a browser. These globals are missing there: - `window` and everything hanging off it, such as `innerWidth`, `matchMedia` and `scrollTo`; - `localStorage` and `sessionStorage`; - `navigator`, `location` and the global `document`. Reading any of them at module load, in a constructor, in a field initializer or in `ngOnInit` throws `ReferenceError: window is not defined` (or the equivalent) on the server. The render of that request fails. ## The server-safe tools | Tool | Runs on server? | Use it for | |---|---|---| | `afterNextRender(cb)` | No, it is skipped | One-off DOM reads and writes, measuring, starting a DOM library | | `afterEveryRender(cb)` | No, it is skipped | Work after every render, such as syncing a canvas | | `inject(DOCUMENT)` | Yes, a server-side DOM | Creating or reading elements, head tags, the root element | | `isPlatformBrowser(inject(PLATFORM_ID))` | Yes, it returns false | Branching logic that is not tied to rendering | | Separate server and browser providers | Yes, the server one | Services whose whole behaviour differs by platform | ## Rewriting the broken component The fix for a layout decision based on the viewport width: 1. Render a sensible default on both sides, so the server HTML and the first browser render agree. 2. Measure in `afterNextRender()`, which only runs in the browser once the view has rendered. 3. Store the result in a signal, so the template updates after hydration. ```ts import { Component, afterNextRender, signal } from '@angular/core'; @Component({ selector: 'app-gallery', template: `<section [class.compact]="compact()">...</section>`, }) export class Gallery { compact = signal(false); // same default on server and browser constructor() { afterNextRender(() => { this.compact.set(window.innerWidth < 600); }); } } ``` Referencing `window` inside the callback is safe because the callback never runs on the server. Where CSS media queries can express the rule, they are better still: no script is needed at all. ## Pitfalls to avoid - **Guarding with `typeof window !== 'undefined'`.** It works, but it hides platform logic in scattered checks. Angular's own tools make the intent visible and testable. - **Rendering different content per platform.** An `@if` that shows one thing on the server and another in the browser makes the DOM differ at hydration, which causes hydration errors and layout shift. Keep rendered output identical, and change it after hydration. - **Third-party libraries that touch `window` on import.** Loading them at the top of a file breaks the server even if you never call them there. Import them dynamically inside `afterNextRender()`, or provide a server stub. - **Assuming the server DOM has layout.** `DOCUMENT` on the server is a DOM implementation without a layout engine, so sizes and positions are meaningless there. ## Checking a codebase for SSR safety When SSR is added to an existing app, a quick audit finds most problems before they reach production: 1. Search for `window.`, `document.`, `localStorage`, `sessionStorage` and `navigator.` outside `afterNextRender()` callbacks and browser-only services. 2. Check field initializers and constructors of root services, which run as soon as anything injects them on the server. 3. List third-party libraries imported at the top of component files and check whether they touch browser globals when imported. 4. Render every route on the server in CI (prerendering or an SSR smoke test), so a new global access fails the build instead of a live request. ## Why interviewers ask it It is the first thing that breaks when a team adds SSR to an existing app. A good answer names the cause (the constructor runs in Node), gives `afterNextRender` as the default fix, and knows when `DOCUMENT` or a platform check fits better.

  • Why is afterNextRender safe to reference window in, even though the component also runs on the server?
    On the server `afterNextRender()` returns a no-op reference and never schedules the callback. In the browser it runs the callback once, after the next render finishes. So code inside it only ever executes where `window` exists, without any explicit platform check.
  • A charting library throws on the server as soon as it is imported. What do you do?
    Stop importing it at the top of the file. Load it with a dynamic `import()` inside `afterNextRender()`, so the module is only evaluated in the browser, or hide it behind a service with a server implementation that does nothing. Both keep the library's global access out of the server bundle's execution path.

saying these in an interview costs you the question

  • SSR only renders templates, so constructors never run on the server.
  • Angular polyfills window and localStorage on the server automatically.
  • Wrapping the template in @if (isBrowser) is the clean fix.
  • afterNextRender runs on the server first and then again in the browser.
  • The server DOM can measure element sizes like a browser.