The Strategy pattern is a staple of object-oriented design, and countless articles and videos demonstrate how it can enhance your Angular applications. However, most of these tutorials stick to plain TypeScript examples, neglecting the unique leverage that Angular’s dependency-injection (DI) framework provides.
Consider the classic TypeScript example from Refactoring Guru: https://refactoring.guru/design-patterns/strategy/typescript/example. Many web snippets are direct reproductions of that code. While functional, they miss an opportunity to use the framework's strengths. Let’s explore how to elevate this pattern into a robust solution that aligns with Angular's conventions, rather than simply embedding standard OOP inside it.
In essence: Strategy defines a set of interchangeable algorithms—classes implementing a shared contract—that can be swapped at runtime.
Take a common scenario: HTTP requests can fail in many distinct ways, from 409 Conflict and 500 Internal Error to custom business codes like 400200. A typical starting point is to route all failures into a single monolithic service, using a long switch statement to determine the appropriate toast, redirect, or retry action. This often becomes a "god object" with tight coupling and a tangle of dependencies. Modifying one error path means altering existing code and risking unintended side effects. The Strategy pattern helps dismantle this complexity! Each error type can reside in its own dedicated class, registered once, allowing a dispatcher to select the correct strategy at runtime—without modifying the calling code.
Applying Strategy to error handling
First, we need to establish the error domain—these codes are the keys that determine which algorithm gets executed:
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 for all strategies
export interface ExtendedServerErrorResponse {
message: string;
errorCode: ErrorCode;
}
export interface ErrorHandlerInterface {
handle(err: HttpErrorResponse | ExtendedServerErrorResponse): void;
}
Abstract helper to avoid repetition
The MatSnackBar (or any common dependency) is injected solely here, giving every concrete strategy easy access to it.
@Injectable()
export abstract class BaseErrorHandlerModel implements ErrorHandlerInterface {
protected readonly snackBar = inject(MatSnackBar);
abstract handle(
err: HttpErrorResponse | ExtendedServerErrorResponse
): void;
}
Creating 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,
});
}
}
While each handler adheres to the same interface, they have the freedom to inject additional services, trigger side effects, or completely alter the logic inside handle(). This results in a well-organized, testable class for each error type, instead of a messy “god service”.
Option A — Service Locator + errorInterceptor
Some context on the Service Locator
The Service Locator is a pattern that offers a central registry (the “locator”) from which the application retrieves dependencies at runtime.
Rather than accepting dependencies through constructor injection, a client invokes locator.get(MyServiceToken) to receive the needed instance.
The easiest method to incorporate our strategies into Angular is to use a static map in conjunction with the framework’s lower-level Injector to create a basic Service Locator arrangement.
- The
errorInterceptorintercepts every failed HTTP response. - It searches for the error code within a global
ERROR_HANDLER_MAP. - By calling
injector.get(token), it retrieves the specific strategy from DI. - Finally, it executes the strategy’s
handle()method.
Here is the corresponding code.
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 implementation
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 - Interceptor registration
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 method works well when your error codes are limited, fixed, and require a single, auditable location for management. For projects that are more dynamic or plugin-based, we can explore a Dispatcher / self-registration model—this is covered in Option B.
Option B — Dispatcher + self-registration
Whereas the Service Locator fetches a handler by its key, this variant allows the handler to declare its own key. Angular’s multi-provider feature collects all implementations, and a small dispatcher determines which one to invoke at runtime.
- Each concrete strategy specifies a
codesarray containing the error codes it manages. - All strategies are registered under the same DI token using
multi: true. - At bootstrap, Angular supplies an array of strategies to the dispatcher.
- The dispatcher creates an in-memory map (
Map<code, strategy>) for quick lookup. - 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 },
],
};
Choose this approach for larger, fluctuating projects or plugin-style architectures—situations where the set of strategies changes frequently and editing a central map each time is undesirable.
Option B is the preferred choice when you aim for a modular, scalable design. Since strategies self-register, you can add, remove, or distribute them across lazy-loaded feature modules without altering centralized code. This results in a highly adaptable system that grows with your application.
Let's test the system's flexibility by making some adjustments. Suppose we currently have a single handler for one error code. What if we need multiple handlers to execute for the same code? Imagine a Lego set, where we can attach several handlers for a single code or event.
This is straightforward to implement.
Simply switch to an array-based map—the rest of the code remains unchanged.
@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));
}
}
This adjustment allows us to assign the same error code to different handlers. This is particularly useful for logging, separating concerns, and achieving highly cohesive code.
Final thoughts
-
Variant A (Service Locator) — compact, explicit, suited for a small,
static set of error codes that can be reviewed in a single file. -
Variant B (Dispatcher + self-registration) — requires minimal upkeep
as your application grows; new strategies are detected automatically, can be
housed in separate libraries, and can even be chained for the same code.
What other areas can benefit from the Strategy + Angular DI combination?
- Transport layers HTTP errors, WebSocket event types, server-sent events, GraphQL subscriptions.
- Front-end messaging native DOM events,
postMessagefor cross-window communication. - Integration points payment gateways, file-export formats, rich-text renderers, feature-flag treatments, and analytics sinks.
- UI behaviour role-based component variants, theme renderers, and data-grid cell editors.
The Strategy pattern is best applied when handling numerous variants. If your situation is a basic if / else, stick with simple code. However, as the list of variants starts to grow—or is expected to—implement Option B and let your architecture scale itself.

