Angular pipes: built-in, async pipe, and pure vs impure (performance + pitfalls)

MediumIntermediateAngular
Preparing for interviews?

Use guided tracks for structured prep, then practice company-specific question sets when you want targeted interview coverage.

Quick Answer

Angular pipes transform values in templates using the | operator (formatting, slicing, mapping) without mutating the source. Most pipes are pure (run only when input reference/primitive changes). The async pipe is special: it subscribes/unsubscribes automatically and triggers view updates.

Answer

Core idea

A pipe is a reusable template-level transformer: it takes an input value and returns a derived value for display. It should be side-effect free and should not mutate the input.

Concept

What it means in practice

Syntax

{{ value | pipeName:arg1:arg2 }} (you can pass parameters)

Chaining

{{ value | pipeA | pipeB }} (output of A becomes input of B)

Where it runs

In Angular’s change detection update phase (template evaluation).

What it’s for

Formatting + small/cheap view transforms; not heavy computation or side effects.

How pipes show up in real templates
HTML
<p>{{ today | date:'fullDate' }}</p>
<p>{{ username | uppercase }}</p>
<p>{{ price | currency:'EUR':'symbol':'1.2-2' }}</p>
<p>{{ completion | percent:'1.0-0' }}</p>
<pre>{{ user | json }}</pre>
                  

Built-in pipe

Typical use

date

Date/time formatting (uses locale + optional timezone)

currency

Money formatting (currency code, display, digitsInfo)

number / decimal

Numeric formatting with digitsInfo

percent

Percent formatting

json

Debug display of objects (not for production UI)

slice

Substrings / subarrays in templates (keep usage cheap)

keyvalue

Iterate object maps in templates

async

Subscribe to Observable/Promise and render latest value safely

Common built-ins interviewers expect you to mention

The async pipe is the one that matters most in real apps

async subscribes/unsubscribes automatically, avoids manual subscription leaks, and marks the view for update when a new value arrives (works great with OnPush).

TYPESCRIPT
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { AsyncPipe, NgIf } from '@angular/common';
import { Observable, timer, map } from 'rxjs';

@Component({
  selector: 'app-demo',
  standalone: true,
  imports: [NgIf, AsyncPipe],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <p *ngIf="count$ | async as count">Count: {{ count }}</p>
  `
})
export class DemoComponent {
  readonly count$: Observable<number> = timer(0, 1000).pipe(map(t => t));
}
                  

Pure vs impure pipes (performance hotspot)

Type

When it runs

Pros

Cons / risk

Pure (default)

Only when input changes by value (primitives) or by reference (objects/arrays/functions).

Fast; avoids re-running on every CD pass.

If you mutate arrays/objects in place, the pipe may not re-run (because reference didn’t change).

Impure (pure: false)

Every change detection cycle.

Can react to in-place mutation or time-varying global state.

Can become very expensive and cause jank; use sparingly.

Most Angular pipe interview questions are really about change detection cost

Classic pure-pipe gotcha

If you do items.push(...), the array reference stays the same, so a pure pipe may not run again. Prefer immutable updates (items = [...items, newItem]) or move the logic out of the template.

Custom pipes: keep them pure + small

Custom pipes are great for reusable formatting/transforms. Keep them deterministic, no side effects, no DOM, no HTTP. In modern Angular you can make them standalone and import them directly.

TYPESCRIPT
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'titleCaseWords',
  standalone: true
})
export class TitleCaseWordsPipe implements PipeTransform {
  transform(value: string | null | undefined): string {
    if (!value) return '';
    return value
      .trim()
      .split(/\s+/)
      .map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
      .join(' ');
  }
}
                  
TYPESCRIPT
import { Component } from '@angular/core';
import { TitleCaseWordsPipe } from './title-case-words.pipe';

@Component({
  selector: 'app-user',
  standalone: true,
  imports: [TitleCaseWordsPipe],
  template: `{{ name | titleCaseWords }}`
})
export class UserComponent {
  name = 'ada lovelace';
}
                  

Pitfall

What goes wrong

Better approach

Heavy work inside a pipe used in big lists

Even pure pipes can run often enough to hurt if the transform is expensive.

Precompute in TS, memoize by key, or transform upstream in RxJS/store selectors.

Using an impure pipe to “fix” mutation

Runs every CD cycle; can tank performance.

Prefer immutable state updates or move logic out of template.

Doing side effects in pipes

Unpredictable behavior; multiple calls; hard to test.

Keep pipes pure; side effects belong in services/effects/components.

Manual subscribe in component just to render

Leaks/unsubscribe complexity; extra state variables.

Use async pipe in template.

Calling methods in templates instead of pipes

Method is executed every CD pass; can be worse than a pure pipe.

Use pure pipes for cheap transforms, or cache results explicitly.

What senior candidates mention quickly
Summary

Pipes are template transformers used via |. Default is pure: runs when inputs change by value/reference (fast, but mutation-in-place can fool it). Impure pipes run every CD cycle (use rarely). The async pipe is key: it manages subscriptions and keeps templates clean—especially with OnPush.

Similar questions
Guides
6 / 37