There are countless videos and blog posts explaining what the Strategy pattern is and how it can improve your Angular code. Yet almost every tutorial presents the pattern in plain TypeScript and quietly ignores the super‑power that Angular’s dependency‑injection (DI) system gives us.
Take the canonical TypeScript sample from Refactoring Guru: https://refactoring.guru/design-patterns/strategy/typescript/example. You’ll find dozens of copy‑and‑paste versions of that snippet on the web—and they work fine for what they are. Let’s take the next step and see how we can turn the pattern into a production‑ready solution that embraces the framework instead of re‑implementing vanilla OOP inside it.
In one sentence: Strategy is a family of interchangeable algorithms—classes that share the same contract—between which we can switch at run time.
Consider a familiar problem: every HTTP request can fail in dozens of different ways—409 Conflict, 500 Internal Error, or a custom business code like 400200. A common first approach is to funnel all errors into one gigantic service and use a long switch to decide what toast, redirect, or retry logic to run. Very quickly that blob turns into a god‑object with tight coupling and sprawling dependencies. Adding or changing a single error path means editing existing code and praying nothing else breaks. The Strategy pattern lets us break that monster apart! Each error variant lives in its own class, we register them once, and the dispatcher chooses the right strategy at run time—with zero changes to the calling code.
Tackling the error-handling problem with Strategy
First, let’s introduce the error domain—these values are the keys we’ll use at run time to pick the right algorithm:
export const ERROR_CODE = {
NotFoundError: '400200',
ServerError: '500200',
Conflict: '409',
Default: '0',
} as const;
export type ErrorCode = typeof ERROR_CODE[keyof typeof ERROR_CODE];
The contract every strategy must follow
export interface ExtendedServerErrorResponse {
message: string;
errorCode: ErrorCode;
}
export interface ErrorHandlerInterface {
handle(err: HttpErrorResponse | ExtendedServerErrorResponse): void;
}
An abstract helper to remove boilerplate
MatSnackBar
(or any shared dependency) lives only here, so every concrete strategy gets it “for free”.
@Injectable()
export abstract class BaseErrorHandlerModel implements ErrorHandlerInterface {
protected readonly snackBar = inject(MatSnackBar);
abstract handle(
err: HttpErrorResponse | ExtendedServerErrorResponse
): void;
}
Two concrete strategies
@Injectable({ providedIn: 'root' })
export class ConflictErrorHandlerService
extends BaseErrorHandlerModel
{
override handle(err: ExtendedServerErrorResponse): void {
this.snackBar.open(`CONFLICT • ${err.message}`, 'close', {
duration: 3000,
});
}
}
@Injectable({ providedIn: 'root' })
export class DefaultErrorHandlerService
extends BaseErrorHandlerModel
{
override handle(err: HttpErrorResponse): void {
this.snackBar.open(`DEFAULT • ${err.message}`, 'close', {
duration: 3000,
});
}
}
Each handler implements the same interface yet is free to inject extra services, perform side effects, or completely change the body of handle(). The result is a clean, testable class per error type instead of one “god service”.
Variant A — Service Locator + errorInterceptor
А few words about the service locator
Service Locator is a design pattern that provides a central registry (the “locator”) from which the rest of the application pulls dependencies at run time.
Instead of receiving collaborators via constructor injection, a client calls locator.get(MyServiceToken)
and gets back the concrete instance it needs.
The simplest way to plug our strategies into Angular is to keep a hard-wired map and combine it with the framework’s low-level Injector
to implement a simple Service Locator
setup.
errorInterceptor
intercepts every failed HTTP response.- It looks up the error code in a global
ERROR_HANDLER_MAP
. - Using
injector.get(token)
it pulls the concrete strategy from DI. - The strategy’s
handle()
is executed.
And here is the code sample.
1 - The global map
import { ProviderToken } from '@angular/core';
import { ERROR_CODE, ErrorCode } from '../models/error-codes';
import { BaseErrorHandlerModel } from '../models/base-error-handler.model';
import { ConflictErrorHandlerService } from './conflict-error-handler.service';
import { DefaultErrorHandlerService } from './default-error-handler.service';
type ErrorHandlerMap =
Partial<Record<ErrorCode, ProviderToken<BaseErrorHandlerModel>>>;
export const ERROR_HANDLER_MAP: ErrorHandlerMap= {
[ERROR_CODE.Conflict]: ConflictErrorHandlerService,
[ERROR_CODE.Default] : DefaultErrorHandlerService,
} as const;
2 - The Service Locator itself
import { Injectable, Injector } from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';
import { ExtendedServerErrorResponse } from '../models/http-response';
import { ErrorCode, ERROR_CODE } from '../models/error-codes';
import { BaseErrorHandlerModel } from '../models/base-error-handler.model';
import { ERROR_HANDLER_MAP } from './error-handler-map.constant';
@Injectable({ providedIn: 'root' })
export class ErrorHandlerLocator {
constructor(private injector: Injector) {}
handle(
code: ErrorCode,
err: HttpErrorResponse | ExtendedServerErrorResponse
): void {
const token =
ERROR_HANDLER_MAP[code] ?? ERROR_HANDLER_MAP[ERROR_CODE.Default];
this.injector.get<BaseErrorHandlerModel>(token).handle(err);
}
}
3 - The functional interceptor
import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
import { inject } from '@angular/core';
import { catchError, throwError } from 'rxjs';
import { ErrorHandlerLocator } from '../handlers/error-handler.locator';
import { ExtendedServerErrorResponse } from '../models/http-response';
import { ErrorCode } from '../models/error-codes';
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
const locator = inject(ErrorHandlerLocator);
return next(req).pipe(
catchError((err: HttpErrorResponse | ExtendedServerErrorResponse) => {
const code = (
(err as ExtendedServerErrorResponse).errorCode ??
(err as HttpErrorResponse).status.toString()
) as ErrorCode;
locator.handle(code, err);
return throwError(() => err);
})
);
};
4 - Registering the interceptor
import { ApplicationConfig } from '@angular/core';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { provideAnimations } from '@angular/platform-browser/animations';
import { errorInterceptor } from './http/error-interceptor.interceptor';
export const appConfig: ApplicationConfig = {
providers: [
provideAnimations(),
provideHttpClient(withInterceptors([errorInterceptor])),
],
};
This approach is perfect when the list of error codes is small, stable, and you want one central place to audit who handles what. For more dynamic or plugin-style projects we’ll switch to a Dispatcher / self-registration model—but that’s Variant B.
Variant B — Dispatcher + self-registration
Where Service Locator pulls a handler by key, this flavour pushes the key into the handler itself. Angular’s multi-provider feature collects every implementation for us, and a tiny dispatcher decides which one to run at runtime.
- Each concrete strategy exposes a
codes
array with the error codes it owns. - All strategies are registered under the same DI token with
multi: true
. - On bootstrap Angular injects an array of strategies into the dispatcher.
- The dispatcher builds an in-memory map (
Map<code, strategy>
). - The interceptor injects the dispatcher and simply calls
dispatch(code, err)
.
1 – DI token
import { InjectionToken } from '@angular/core';
import { ErrorHandlerStrategy } from '../models/error-handler.interface';
export const ERROR_HANDLER_TOKEN =
new InjectionToken<ErrorHandlerStrategy[]>('ERROR_HANDLER_TOKEN');
2 – Strategy classes (now with codes!)
@Injectable({ providedIn: 'root' })
export class ConflictErrorHandlerService
extends BaseErrorHandlerModel
implements ErrorHandlerStrategy
{
readonly codes = [ERROR_CODE.Conflict];
override handle(err: ExtendedServerErrorResponse): void {
this.snackBar.open(`CONFLICT • ${err.message}`, 'close', { duration: 3000 });
}
}
@Injectable({ providedIn: 'root' })
export class DefaultErrorHandlerService
extends BaseErrorHandlerModel
implements ErrorHandlerStrategy
{
readonly codes = [ERROR_CODE.Default];
override handle(err: HttpErrorResponse): void {
this.snackBar.open(`DEFAULT • ${err.message}`, 'close', { duration: 3000 });
}
}
3 – The dispatcher
import { Inject, Injectable } from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';
import { ERROR_HANDLER_TOKEN } from '../tokens/error-handler-token';
import { ErrorHandlerStrategy } from '../models/error-handler.interface';
import { ErrorCode, ERROR_CODE } from '../models/error-codes';
import { ExtendedServerErrorResponse } from '../models/http-response';
@Injectable({ providedIn: 'root' })
export class ErrorHandlerDispatcher {
private readonly map = new Map<ErrorCode, ErrorHandlerStrategy>();
constructor(
@Inject(ERROR_HANDLER_TOKEN) strategies: ErrorHandlerStrategy[]
) {
for (const s of strategies)
for (const c of s.codes) this.map.set(c, s);
}
dispatch(
code: ErrorCode,
err: HttpErrorResponse | ExtendedServerErrorResponse
): void {
const strategy =
this.map.get(code) ?? this.map.get(ERROR_CODE.Default);
strategy?.handle(err);
}
}
4 – Functional interceptor
import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
import { inject } from '@angular/core';
import { catchError, throwError } from 'rxjs';
import { ErrorHandlerDispatcher } from '../handlers/error-handler.dispatcher';
import { ExtendedServerErrorResponse } from '../models/http-response';
import { ErrorCode } from '../models/error-codes';
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
const dispatcher = inject(ErrorHandlerDispatcher);
return next(req).pipe(
catchError((err: HttpErrorResponse | ExtendedServerErrorResponse) => {
const code = (
(err as ExtendedServerErrorResponse).errorCode ??
(err as HttpErrorResponse).status.toString()
) as ErrorCode;
dispatcher.dispatch(code, err);
return throwError(() => err);
})
);
};
5 – Registration
import { ApplicationConfig } from '@angular/core';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { provideAnimations } from '@angular/platform-browser/animations';
import { ERROR_HANDLER_TOKEN } from './tokens/error-handler-token';
import { ConflictErrorHandlerService } from './handlers/conflict-error-handler.service';
import { DefaultErrorHandlerService } from './handlers/default-error-handler.service';
import { errorInterceptor } from './http/error-interceptor.interceptor';
export const appConfig: ApplicationConfig = {
providers: [
provideAnimations(),
provideHttpClient(withInterceptors([errorInterceptor])),
// self-registering strategies
{ provide: ERROR_HANDLER_TOKEN, useExisting: ConflictErrorHandlerService, multi: true },
{ provide: ERROR_HANDLER_TOKEN, useExisting: DefaultErrorHandlerService, multi: true },
],
};
Use it for larger, evolving projects or plugin-style architectures—anywhere the set of strategies changes often and you don’t want to edit a global map each time.
Variant B is the better fit when you want a modular, future-proof design. Strategies register themselves, so you can add, remove, or ship them in lazy-loaded feature modules without touching central code. The result is a highly flexible system that evolves with your product instead of fighting it.
Let's try to make some changes and see how our system works and how ready it is for changes. Now we have one handler for the error code. But what if we want to execute several handlers for one error code. Let's say we make Lego, when we can have several handlers for one code or event.
It's very easy to do.
Just switch to an array-based map—the rest of the code stays untouched.
@Injectable({ providedIn: 'root' })
export class ErrorHandlerDispatcher {
private readonly map = new Map<ErrorCode, ErrorHandlerStrategy[]>();
constructor(
@Inject(ERROR_HANDLER_TOKEN) strategies: ErrorHandlerStrategy[]
) {
for (const s of strategies) {
for (const c of s.codes) {
const arr = this.map.get(c) ?? [];
arr.push(s);
this.map.set(c, arr);
}
}
}
dispatch(
code: ErrorCode,
err: HttpErrorResponse | ExtendedServerErrorResponse
): void {
const list = this.map.get(code) ?? this.map.get(ERROR_CODE.Default);
list?.forEach(h => h.handle(err));
}
}
With this approach, we can specify the same error code for different handlers. This can be very useful for logging, for separating responsibilities, and for writing the most coherent code possible.
Final takeaway
-
Variant A (Service Locator) — tiny, explicit, great for a short,
immutable list of error codes that security can audit in one file. -
Variant B (Dispatcher + self-registration) — virtually zero maintenance
as your app grows; new strategies appear automatically, can live in
separate libraries, and can even stack together for the same code.
Where can Strategy + Angular DI shine?
- Transport layers HTTP errors, WebSocket event types, server-sent events, GraphQL subscriptions.
- Front-end messaging native DOM events,
postMessage
cross-window communication. - Integration points payment gateways, file-export formats, rich-text renderers, feature-flag treatments, analytics sinks.
- UI behaviour per-role component variants, theme renderers, data-grid cell editors.
The pattern is a hammer only when you have many nails. If your use-case is a single if / else
, keep the code simple. But once the list of variants starts creeping up—or might tomorrow—drop in Variant B and let the architecture scale itself.
