skip to content

In Angular, what does inject(Token, {optional: true}) return when no injector provides the token, and how does that differ from a plain inject() call?

level: juniorimportance: should knowfreq 45%

answer

  1. missing provider, two outcomes
  2. null instead of an error
  3. return type widens with the option
  4. search path unchanged
  5. decorator form on constructors

basics

~20 s

With optional: true, Angular returns null when the whole search finds no provider; a plain inject() throws NG0201 instead. The option changes only the not-found outcome, not where Angular searches, and its constructor equivalent is @Optional().

solid answer

~40 s

By default a lookup that reaches the top of the injector walk without a match throws `NG0201: No provider found for ...`. Passing `{optional: true}` makes that same miss return `null`, and TypeScript types the result as `T | null`, so the caller must handle the absent case (`this.sink?.track(...)`). It does not change **where** Angular searches: element injectors, then environment injectors, as usual — and a provider that exists but whose factory throws still throws. The constructor-parameter equivalent is `@Optional()`. Use it for genuinely optional collaborators — a plugin, an analytics sink, a parent that may not exist — and combine it with `self`, `skipSelf` or `host`, which make misses more likely. Do not use it to silence a required dependency: you trade a clear NG0201 at creation for a null crash later.

code

ts · 21 lines
ts
import {Component, InjectionToken, inject, signal} from '@angular/core';

export interface AnalyticsSink {
  track(event: string): void;
}
export const ANALYTICS_SINK = new InjectionToken<AnalyticsSink>('ANALYTICS_SINK');

@Component({
  selector: 'app-accordion-panel',
  template: `<button (click)="toggle()">{{ open() ? 'Hide' : 'Show' }}</button>`,
})
export class AccordionPanel {
  readonly open = signal(false);
  // AnalyticsSink | null: null when the app did not provide a sink
  private sink = inject(ANALYTICS_SINK, {optional: true});

  toggle() {
    this.open.update((v) => !v);
    this.sink?.track('accordion-toggle');
  }
}

go deeper

for a junior

Recall that optional injection returns null instead of throwing NG0201, and that you must handle the null in code.

for a middle

Explain that optional changes only the not-found outcome, not the search path, and name the @Optional() decorator as its constructor form.

for a senior

Show judgement about when absence is legitimate, and prefer a defaulted token or a fixed provider placement over silencing a required dependency.

for a principal

Set library conventions for extension points: which hooks are optional, which have defaults, and how that keeps misconfiguration errors loud across teams.

## The default: a miss is an error When Angular resolves a **token** — a class or an `InjectionToken` — it searches element injectors from the requesting element upward, then environment injectors up to `root` and the platform injector. If nothing matches, the lookup reaches the `NullInjector`, which throws: ```text NG0201: No provider found for `AnalyticsSink`. ``` In Angular 22 the message also carries a `Path:` listing the injection chain. Throwing is the right default: a component that silently runs without a required service fails later and farther from the cause. ## What `optional` changes `inject(token, {optional: true})` changes exactly one thing: **what happens on a miss**. | | `inject(Sink)` | `inject(Sink, {optional: true})` | |---|---|---| | Provider found | the instance | the instance | | No provider anywhere on the path | throws NG0201 | returns `null` | | Provider found but its factory throws | throws | throws | | Static type | `Sink` | `Sink \| null` | | Search order | unchanged | unchanged | Points to be precise about in an interview: - It returns **`null`**, not `undefined` — code that checks `=== undefined` misses it. - The return type widens to `T | null` through `inject()`'s overloads, so strict TypeScript forces you to handle the absent case. - It is **not** a scope modifier: Angular still searches the full path. Whether a provider is *reachable* is decided by where providers sit and by the other options. ## The decorator form Before `inject()` became the recommended style, the same behaviour came from parameter decorators on constructors: ```ts constructor(@Optional() @Inject(ANALYTICS_SINK) sink: AnalyticsSink | null) {} ``` `@Optional()` is still supported and maps to the same lookup flag. The numeric `InjectFlags` enum that used to express these options was removed in Angular 20; `inject()` and `Injector.get()` take the options object instead. ## When `optional` is the right tool 1. **Optional collaborators.** An accordion panel that reports toggles to an analytics sink if the host application configured one, and works without it otherwise. 2. **Plugin hooks.** A library that checks whether the app provided a customisation token and falls back to built-in behaviour. 3. **Parent discovery.** A component looking for an ancestor of its own type — the top-level instance has none — usually as `{optional: true, skipSelf: true}`. 4. **Narrowed lookups.** `self`, `skipSelf` and `host` limit where Angular looks, so a miss becomes a normal outcome; pairing them with `optional` turns that miss into `null` instead of an exception. ## When it is the wrong tool - **Hiding a missing provider.** If the feature cannot work without the service, a `null` check only postpones the failure to a `TypeError` deep in a handler. - **Supplying a default.** If you want a default value rather than `null`, give the token a default instead — an `InjectionToken` with a `factory`, or a root-provided default implementation — so every consumer gets something usable without null checks. - **Masking the wrong scope.** If a provider exists but sits on a sibling branch or a different route, `optional` makes the bug silent; fix where the provider is placed. ## Interaction with the other options `optional` is most often seen next to an option that narrows the search: - `{optional: true, skipSelf: true}` — "my parent's instance, if I have a parent", used by recursive widgets. - `{optional: true, self: true}` — "a collaborator on this same element, if one is there", used by directives that enhance a form control only when one exists. - `{optional: true, host: true}` — "something from the template that declares me, if it provides it". In each case the narrowing option decides **where** Angular looks and `optional` decides **what a miss means**. Keeping those two questions separate is the cleanest way to explain the whole family of options. ## Summary `optional` is a statement about the *domain* — "this dependency may legitimately be absent" — not a workaround for DI errors. Used that way, it keeps NG0201 meaningful for everything else.

  • Does optional: true make Angular search fewer or different injectors?
    No. The search path is exactly the same as without the option: element injectors from the requester upward, then environment injectors. `optional` only replaces the NG0201 thrown on a miss with `null`. To change where the search starts or stops you use `skipSelf`, `self` or `host`, often together with `optional`.
  • When would you give a token a default instead of injecting it optionally?
    When consumers should always get a usable value. An `InjectionToken` declared with a `factory` (and `providedIn: 'root'`) supplies a default that apps can override with their own provider, so no consumer needs a null check. Optional injection fits better when absence itself changes behaviour, such as skipping analytics entirely.

saying these in an interview costs you the question

  • optional: true returns undefined when nothing is provided
  • optional restricts the search to the current component
  • optional also swallows errors thrown by the provider's factory
  • Wrapping required services in optional is a safe default
  • InjectFlags.Optional is the current way to pass the option