skip to content

An Angular SSR page throws NG0500 during hydration after a third-party carousel library rewrites its slides. Why, and how would you fix it?

level: seniorimportance: should knowfreq 45%

answer

  1. node-by-node matching
  2. DOM changed before hydration
  3. NG0500 vs NG0501 vs NG0502
  4. fix markup, defer, or isolate

basics

~10 s

Hydration expects the browser DOM to match the server's; the carousel restructured it before Angular hydrated. Let Angular render the slides, initialize the library after hydration, or wrap it in a component marked ngSkipHydration.

solid answer

~40 s

Non-destructive hydration walks the server-rendered DOM node by node against serialized annotations. A carousel library that wraps slides, clones them or moves nodes before hydration reaches the component leaves Angular finding the wrong node (NG0500), too few siblings (NG0501) or no node (NG0502). I would reproduce it in development mode, where the detailed checks and messages run, and confirm with Angular DevTools. Then, in order: render the slides with `@for` and drive them with bindings; failing that, move the library's DOM-changing initialization to browser-only code that runs after rendering; and as a last resort wrap the carousel in its own component with `ngSkipHydration` on the host, accepting a local re-render. I would also check the template for invalid HTML nesting, another common mismatch source.

code

ts · 16 lines
ts
import { Component, input } from '@angular/core';

@Component({
  selector: 'app-product-carousel',
  template: `
    <div class="track" [style.transform]="'translateX(' + -index() * 100 + '%)'">
      @for (slide of slides(); track slide.id) {
        <img [src]="slide.src" [alt]="slide.alt" />
      }
    </div>
  `,
})
export class ProductCarousel {
  slides = input.required<{ id: string; src: string; alt: string }[]>();
  index = input(0);
}

go deeper

for a junior

Recall that hydration needs the browser DOM to match the server's, and that code changing the DOM early breaks it.

for a middle

Explain what NG0500, NG0501 and NG0502 each report and the common causes: direct DOM manipulation, invalid HTML and side-dependent content.

for a senior

Diagnose the failing component in development mode, rank the fixes, and scope ngSkipHydration to a wrapper while tracking it as debt.

for a principal

Set rules for adopting DOM-owning libraries in a hydrated app, and decide when wrapping, replacing or rewriting a widget is worth it.

## The scenario A marketing page shows a product carousel from a third-party library. The page is server-rendered and hydrated. In the browser console, hydration fails with **NG0500 (node mismatch)**, sometimes **NG0501 (missing siblings)** or **NG0502 (missing node)**, pointing into the carousel component. ## Why it happens Non-destructive hydration assumes that the DOM in the browser is **exactly** the DOM the server produced. Angular walks it node by node, using the serialized annotations to find each element, text node and comment it created. A carousel library typically initializes by: - wrapping slides in extra track and viewport elements; - cloning the first and last slides for infinite scrolling; - moving nodes into a new container, or setting `innerHTML`. If that code runs **before** hydration reaches the component (for example in the constructor or `ngOnInit`, or from a script that runs as soon as the page loads), Angular finds a wrapper where it expected a slide, or fewer siblings than it serialized. The three codes describe which expectation failed: | Code | What Angular found | | --- | --- | | NG0500 | A node, but not the kind or tag it expected | | NG0501 | Fewer sibling nodes than it expected at that position | | NG0502 | No node at all where one should be | ## How to diagnose it 1. Run in **development mode**: the detailed mismatch checks and messages are dev-mode only, and production builds skip most of them. 2. Read the error: it prints the expected and actual DOM around the failure and names the component. 3. Use Angular DevTools, which highlights the component where the mismatch happened. 4. Temporarily disable the library's initialization; if hydration succeeds, the library is the cause. 5. Check the other usual suspects in that template: invalid nesting such as a `<div>` inside a `<p>`, or a table without `<tbody>`, which the browser parser repairs differently from the server output. ## How to fix it, in order of preference 1. **Let Angular own the markup.** Render slides with `@for` and drive position with bindings or CSS transforms instead of letting the library restructure the DOM. 2. **Initialize the library after hydration.** Moving the DOM-changing call out of the constructor or `ngOnInit`, into a callback that runs only in the browser after rendering (the browser-only code paths topic covers the API), means hydration sees the untouched server DOM. 3. **Isolate the subtree.** Wrap the carousel in its own component and add `ngSkipHydration` to that wrapper's host element. The wrapper then re-renders from scratch on the client while the rest of the page hydrates. Put it on the component host, not on an inner `<div>`, or Angular throws NG0504. Option 2 has a caveat: after the library has rearranged the DOM, Angular's later updates to that subtree may no longer find the nodes where it put them. Keep the library's container free of Angular bindings, or prefer option 1. ## Related mismatch causes worth naming - **Conditional content that differs by side.** An `@if` or `@for` whose value is copied once into a plain `signal()` in `ngOnInit`, rather than derived with `computed()` from the input, can produce a different number of nodes on server and client, which shows up as NG0501. - **Platform-dependent branches.** An `@if` that renders different content on the server and in the browser produces a mismatch and a layout shift. ## What a senior answer shows - It explains the **mechanism** (node-by-node matching against serialized annotations), not just the error code. - It ranks the fixes and treats `ngSkipHydration` as a scoped, temporary cost. - It knows the detailed checks are dev-mode only, so hydration must be tested before production.

  • Why do the mismatch errors sometimes not appear in production?
    The detailed node validation runs only in development mode; production builds skip most of it to save work and throw coded errors only in a few cases. A mismatch can therefore surface as broken behaviour rather than a clear message, so hydration must be tested in development.
  • Can NG0501 happen without any third-party DOM code?
    Yes. If an `@if` or `@for` depends on a value copied once into a plain `signal()` in `ngOnInit`, the server and client can render different numbers of nodes. Deriving it with `computed()` from the input keeps both sides consistent.

saying these in an interview costs you the question

  • Hydration mismatches are caused by slow networks and fix themselves on reload
  • Put ngSkipHydration on the carousel's inner div to fix it
  • Angular merges unexpected DOM nodes into its view during hydration
  • Production builds report the same detailed mismatch messages as development
  • Only third-party libraries can cause NG0500-family errors