DI Error Handling — Reference
@tagna/udiot v1.0.0-rc.1
Error Hierarchy
All DI errors extend the DIError base class, which itself extends the native Error. This makes it straightforward to catch only DI errors while letting other errors propagate.
Error
└── DIError (base for all DI errors)
├── CircularDependencyError
├── ProviderNotFoundError
└── InvalidProviderError
DIError
class DIError extends Error {
name: 'DIError';
}
The base class. Catch DIError to handle any DI-related failure as a group.
import { DIError } from '@tagna/udiot';
try {
container.resolve(SomeService);
} catch (err) {
if (err instanceof DIError) {
console.error('DI failure:', err.message);
} else {
throw err; // re-throw unrelated errors
}
}
CircularDependencyError
class CircularDependencyError extends DIError {
name: 'CircularDependencyError';
// message: "Circular dependency detected: A -> B -> A"
}
When thrown: container.resolve() detects that resolving token T would require resolving T again (directly or transitively).
The error message contains the full dependency chain joined by ->. The chain includes the token that closed the cycle as the last entry.
CircularDependencyError: Circular dependency detected: OrderService -> UserService -> OrderService
Example that triggers the error:
@Injectable()
class ServiceA {
constructor(private b: ServiceB) {}
}
@Injectable()
class ServiceB {
constructor(private a: ServiceA) {} // A → B → A
}
container.resolve(ServiceA); // throws CircularDependencyError
Resolution strategies:
- Restructure — extract shared logic into a third class that A and B both depend on without depending on each other.
- Lazy injection — inject a factory
() => ServiceBinstead ofServiceBdirectly. - Property injection — use
@InjectProperty()on one side so the instance is constructed before the circular property is populated.
ProviderNotFoundError
class ProviderNotFoundError extends DIError {
name: 'ProviderNotFoundError';
// message: "No provider found for token: UserService"
}
When thrown: container.resolve(token) is called for a token that is not registered in the container or any ancestor container.
try {
const svc = container.resolve<UserService>(UserService);
} catch (err) {
if (err instanceof ProviderNotFoundError) {
console.error(err.message); // "No provider found for token: UserService"
}
}
Common causes:
- Forgetting to call
registerClass()/registerValue()/registerFactory()before the firstresolve(). - Resolving from a child container for a token that was registered only in a sibling container.
- Typo in a string token.
Using optional resolution to avoid the throw:
function resolveOptional<T>(container: Container, token: InjectionToken): T | undefined {
try {
return container.resolve<T>(token);
} catch (err) {
if (err instanceof ProviderNotFoundError) return undefined;
throw err;
}
}
const cache = resolveOptional(container, CacheService); // undefined if not registered
Alternatively, use @Optional() on a constructor parameter:
@Injectable()
class MyService {
constructor(
@Optional() @Inject('OPTIONAL_CACHE') private cache?: CacheService
) {}
}
InvalidProviderError
class InvalidProviderError extends DIError {
name: 'InvalidProviderError';
}
When thrown in two situations:
- A provider object is passed to
register()ornormalizeProvider()that is neither a function nor an object with aprovidekey. - A
ClassProvider'suseClassis not decorated with@Injectable().
InvalidProviderError: Invalid provider: Class UserService is not injectable.
Add @Injectable() decorator.
Fix for case 1 — malformed provider object:
// Wrong — missing 'provide' key
container.register({ useClass: UserService }); // throws InvalidProviderError
// Correct
container.register({ provide: UserService, useClass: UserService });
Fix for case 2 — missing decorator:
// Wrong — UserService lacks @Injectable()
class UserService { /* ... */ }
container.registerClass(UserService, UserService); // throws at resolve time
// Correct
@Injectable()
class UserService { /* ... */ }
container.registerClass(UserService, UserService); // works
Error Handling Patterns
Pattern 1: Try/Catch Around resolve()
The safest approach when a dependency might not be registered:
import { DIError, ProviderNotFoundError, CircularDependencyError } from '@tagna/udiot';
function safeResolve<T>(container: Container, token: InjectionToken): T {
try {
return container.resolve<T>(token);
} catch (err) {
if (err instanceof CircularDependencyError) {
throw new Error(`Circular dependency in DI graph: ${err.message}`);
}
if (err instanceof ProviderNotFoundError) {
throw new Error(`Missing registration — did you forget to register ${String(token)}?`);
}
throw err; // non-DI error
}
}
Pattern 2: Optional Resolution with @Optional()
Use @Optional() when a dependency is genuinely optional. The container injects undefined instead of throwing.
@Injectable()
class NotificationService {
constructor(
@Optional() @Inject('PUSH_ADAPTER') private push?: PushAdapter,
@Optional() @Inject('EMAIL_ADAPTER') private email?: EmailAdapter
) {}
notify(message: string): void {
this.push?.send(message);
this.email?.send(message);
}
}
Pattern 3: Default Values with Optional Resolution
Combine resolveOptional with a default value for a clean fallback:
const config = resolveOptional<AppConfig>(container, 'APP_CONFIG') ?? {
debug: false,
logLevel: 'warn'
};
Pattern 4: Validation at Startup
Eagerly resolve all required tokens at application startup so missing registrations surface immediately rather than at runtime when a request triggers resolution:
function validateDIGraph(container: Container, requiredTokens: InjectionToken[]): void {
const failures: string[] = [];
for (const token of requiredTokens) {
try {
container.resolve(token);
} catch (err) {
if (err instanceof DIError) {
failures.push(`${String(token)}: ${err.message}`);
} else {
throw err;
}
}
}
if (failures.length > 0) {
throw new Error(`DI validation failed:\n${failures.join('\n')}`);
}
}
// At app startup, before accepting traffic:
validateDIGraph(globalContainer, [
DatabaseService,
UserService,
'APP_CONFIG'
]);