# 02 — Typed exceptions

A typed exception hierarchy: one base + 7 category bases + 7
generic concretes. Project-specific exceptions subclass these.

> **Read first:** [`library_common.md`](README.md).

---

## Motivation

Python's stdlib exceptions are too coarse (`OSError`,
`RuntimeError`) for application-level error handling. Without
a typed hierarchy, every consumer reinvents one — usually as
ad-hoc strings ("if 'not found' in str(e):..."), which is
fragile across translations + locales + library versions.

`mgf.common.exceptions` ships the canonical hierarchy that
every MGF project shares. Consumers subclass the right
category for their domain; cross-project tooling (the CLI
exit-code translator, structured-log formatters, the crash
reporter) reads the category and routes accordingly.

---

## Goal

Every error your app raises is a typed instance of an
`mgf.common.exceptions.*` subclass. Catching the category base
catches every refinement. Machine-readable fields (e.g.
`ResourceNotFoundError.name`, `CommandError.argv`) survive
translation; the message string is for humans.

---

## Quick start

```python
from mgf.common.exceptions import (
    AppError,
    ProviderError,
    ResourceNotFoundError,
)

# Subclass a generic concrete to refine its message:
class DomainNotFoundError(ResourceNotFoundError):
    def __init__(self, name: str) -> None:
        super().__init__(name)
        self.args = (f"no domain named {name!r}; run 'myapp list'",)

# Use it:
def lookup_domain(name: str) -> Domain:
    try:
        return _provider.fetch(name)
    except _provider.NotFound:
        raise DomainNotFoundError(name) from None

# Catch at the right level:
try:
    domain = lookup_domain("corp-net")
except ResourceNotFoundError as e:
    # Catches DomainNotFoundError + any other "resource missing" subclass
    print(f"missing: {e.name}")
except ProviderError:
    # Catches every provider-side failure
    print("provider failed")
except AppError:
    # Catches anything from this library or any consumer's typed error
    print("application error")
```

---

## The hierarchy

```
AppError
├── ConfigError
│ ├── SchemaValidationError carries .errors (list[str])
│ └── AppConfigError config.yaml missing / malformed / unknown version
├── ProviderError third-party SDK boundary failures
│ ├── ResourceNotFoundError carries .name
│ └── ResourceAlreadyExistsError carries .name
├── OperationError long-running operation failures
│ ├── CommandError carries .argv / .returncode / .stderr
│ ├── BootstrapError +; carries .failed_step
│ └── CancellationError +; raised by raise_if_cancelled()
├── GuestError remote-agent communication failures
│ ├── GuestUnreachableError
│ └── GuestCommandError
├── HostEnvironmentError host pre-flight (CPU lacks vmx, kernel module missing)
├── VaultError credential storage failures
└── SchedulerError scheduler / hook subsystem failures
```

`EnvironmentError` is a **deprecated alias** for
`HostEnvironmentError`. Both names
work; the alias emits `MgfDeprecationWarning`.

---

## Public API

| Name | What it carries |
|---|---|
| [`AppError`](../../PUBLIC_API.md#mgfcommonexceptions) | Base. Subclass for project-wide nominal aliases. |
| [`SchemaValidationError(errors)`](../../PUBLIC_API.md#mgfcommonexceptions) | `.errors: list[str]`. Message: `"validation failed: ..."`. Subclass with `_MESSAGE_PREFIX = "spec validation failed"` to override. |
| [`AppConfigError`](../../PUBLIC_API.md#mgfcommonexceptions) | Config file missing / malformed / unknown version. |
| [`CommandError(argv, returncode, stderr)`](../../PUBLIC_API.md#mgfcommonexceptions) | Subprocess wrapper translation target. |
| [`ResourceNotFoundError(name)`](../../PUBLIC_API.md#mgfcommonexceptions) / [`ResourceAlreadyExistsError(name)`](../../PUBLIC_API.md#mgfcommonexceptions) | `.name: str`. |
| [`BootstrapError(failed_step, cause=...)`](../../PUBLIC_API.md#mgfcommonexceptions) | `.failed_step: str`. Cause text composed into message (does NOT chain via `__cause__`). |
| [`CancellationError`](../../PUBLIC_API.md#mgfcommonexceptions) | Subclass of `OperationError`. Default message: `"operation cancelled by user"`. |
| [`HostEnvironmentError`](../../PUBLIC_API.md#mgfcommonexceptions) | Renamed from `EnvironmentError`; alias retained. |

Code:
[`src/mgf/common/exceptions/__init__.py`](../../src/mgf/common/exceptions/__init__.py),
[`_well_known.py`](../../src/mgf/common/exceptions/_well_known.py),
[`_base.py`](../../src/mgf/common/exceptions/_base.py).

---

## Patterns

### Translation seam

When wrapping a third-party SDK, translate at the boundary —
never let the SDK's exception type leak past your wrapper.

```python
from mgf.common.exceptions import CommandError
import subprocess

def run(argv: list[str]) -> str:
    try:
        result = subprocess.run(argv, check=True, capture_output=True, text=True)
    except subprocess.CalledProcessError as e:
        raise CommandError(
            argv=argv,
            returncode=e.returncode,
            stderr=e.stderr or "",
        ) from e
    return result.stdout
```

The CLI translator (see [`09_cli.md`](cli.md)) maps every
typed exception family to a stable exit code; consumers that
re-raise as `CommandError` get correct exit codes for free.

### Error message style

Per `mgf-fed standards ERROR_HANDLING` §14: tone is
descriptive, not blameful. Include identifiers; suggest the fix.

```
no domain named 'corp-net'; run 'myapp domain list' to see available
                ^^^^^^^^^^                          ^^^^^^^^^^^^^^^^
                identifier                          actionable hint
```

NOT: "Domain not found."

### Subclass into a project-wide nominal alias

A `MyAppError` alias lets every domain `except MyAppError:`
clause stay project-flavoured while inheriting the categorical
machinery.

```python
from mgf.common.exceptions import AppError

class MyAppError(AppError):
    """Base for every myapp-specific error."""
```

vm-vmanager-app does this with `vm-vmanager-appError = AppError`.

---

## Links

- **Public API:** [`PUBLIC_API.md` `mgf.common.exceptions` section](../../PUBLIC_API.md#mgfcommonexceptions)
- **Engineering standards:**
  [`ERROR_HANDLING.md`](https://codeberg.org/magogi-admin/mgf-standard/src/branch/main/docs/standards/ERROR_HANDLING.md) — full
  hierarchy spec + translation-seam rules + error-message style guide (EH-01..EH-14)
- **Examples:** [`examples/01_exceptions/`](../../examples/01_exceptions/)
  — three layers (simple / practical / advanced)
- **Code:**
  [`src/mgf/common/exceptions/`](../../src/mgf/common/exceptions/)
- **Related docs:**
  - [`06_crash_reporting.md`](crash_reporting.md) — typed
    exceptions land in crash reports with their machine fields
  - [`09_cli.md`](cli.md) — exit-code translator maps each
    category to a stable numeric code
