Docs/Dependency injectionv1.0.0-rc.1

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:

  1. Restructure — extract shared logic into a third class that A and B both depend on without depending on each other.
  2. Lazy injection — inject a factory () => ServiceB instead of ServiceB directly.
  3. 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 first resolve().
  • 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:

  1. A provider object is passed to register() or normalizeProvider() that is neither a function nor an object with a provide key.
  2. A ClassProvider's useClass is 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'
]);
From the canonical UDIOT public documentation.Try it in the playground ↗