Skip to content

Replace RuntimeError with custom exception hierarchy #177

@lesnik512

Description

@lesnik512

Problem

All errors raised by the framework use the built-in RuntimeError. This forces users to either catch the very broad RuntimeError (which also matches unrelated runtime issues) or rely on string matching to distinguish failure modes.

Current call sites:

  • modern_di/container.py — scope-mismatch, max-scope-reached, missing-provider, skipped-scope, circular-dependency
  • modern_di/providers/factory.py — argument-resolution failures
  • modern_di/registries/providers_registry.py — duplicate provider type
  • modern_di/registries/cache_registry.py — finalizer cleanup errors (sync + async)
  • modern_di/group.pyGroup instantiation attempt

These cover several semantically distinct conditions, but downstream code can't tell them apart programmatically.

Proposed Solution

Introduce a small exception hierarchy in modern_di/errors.py (or a new modern_di/exceptions.py), all rooted at a base ModernDIError(RuntimeError) so existing user code that catches RuntimeError keeps working.

A first-pass mapping that aligns with the existing error templates:

ModernDIError(RuntimeError)
├── ContainerError
│   ├── ScopeMismatchError       # CONTAINER_SCOPE_IS_LOWER_ERROR, CONTAINER_NOT_INITIALIZED_SCOPE_ERROR
│   ├── ScopeSkippedError        # CONTAINER_SCOPE_IS_SKIPPED_ERROR
│   └── MaxScopeReachedError     # CONTAINER_MAX_SCOPE_REACHED_ERROR
├── ResolutionError
│   ├── ProviderNotRegisteredError   # CONTAINER_MISSING_PROVIDER_ERROR
│   ├── ArgumentResolutionError      # FACTORY_ARGUMENT_RESOLUTION_ERROR
│   └── CircularDependencyError      # CYCLE_DEPENDENCY_ERROR
├── RegistrationError
│   └── DuplicateProviderTypeError   # PROVIDER_DUPLICATE_TYPE_ERROR
├── FinalizerError                   # cache_registry sync/async cleanup
└── GroupInstantiationError          # group.py

Each exception type would carry the structured fields it currently formats into a string (e.g. ArgumentResolutionError(arg_name, arg_type, bound_type)), making programmatic introspection possible.

Impact

  • Users can write narrow except CircularDependencyError: / except ProviderNotRegisteredError: blocks instead of matching on RuntimeError message strings.
  • Tests in this repo currently rely on pytest.raises(RuntimeError, match=...) — they can migrate to typed assertions, which are stricter and don't break when wording is improved.
  • Inheriting from RuntimeError preserves backwards compatibility for anyone catching the broad type today.
  • Pairs well with Include dependency path in resolution error messages #170 (dependency-path context) since structured exceptions are a natural place to attach the resolution chain.

Complexity

Low–medium. Mechanical change: define classes, swap raise RuntimeError(...) sites, update tests. No behavior change beyond exception type.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions