# Exception Hierarchy

## Problem

Using generic exceptions (`Exception`, `Error`, `RuntimeError`) everywhere makes it impossible for callers to distinguish between different failure modes. Code ends up catching broad exception types and either over-handling (catching things it shouldn't) or under-handling (missing specific cases). Error messages are the only differentiator, which is fragile and untestable.

## Solution

Organise exceptions into a **class hierarchy** that mirrors your domain's failure categories. Create a base application exception, then derive specific exceptions for each failure mode. Callers can catch at the granularity they need — specific leaf exceptions or broader categories.

```
AppError
├── ValidationError
│   ├── MissingFieldError
│   └── InvalidFormatError
├── AuthError
│   ├── AuthenticationError
│   └── AuthorizationError
└── ExternalServiceError
    ├── TimeoutError
    └── ConnectionError
```

## When to Use

- Applications with multiple distinct failure modes that callers need to handle differently.
- Library/SDK code where consumers need granular error handling.
- When you want to catch a category of errors (e.g., all auth errors) without listing every specific type.

## When to Avoid

- Small scripts with one or two failure modes — a simple `ValueError` suffices.
- Languages that favour Result types over exceptions (Go, Rust).
- When the hierarchy becomes deeper than 3 levels — flatten it.

## Pseudocode

```
class AppError extends Error:
    code: string
    context: map

class ValidationError extends AppError
class MissingFieldError extends ValidationError
class InvalidFormatError extends ValidationError

class AuthError extends AppError
class NotAuthenticatedError extends AuthError
class NotAuthorizedError extends AuthError

// Usage
try:
    process_request(request)
catch AuthError as e:
    respond(401/403, e.message)
catch ValidationError as e:
    respond(400, e.message)
catch AppError as e:
    respond(500, "Internal error")
```

## Python

```python
"""Exception Hierarchy pattern in Python."""


# ── Exception hierarchy ───────────────────────────────────────

class AppError(Exception):
    """Base exception for the application."""
    def __init__(self, message: str, code: str = "APP_ERROR", **context):
        super().__init__(message)
        self.code = code
        self.context = context


class ValidationError(AppError):
    """Base for all validation failures."""
    def __init__(self, message: str, **context):
        super().__init__(message, code="VALIDATION_ERROR", **context)


class MissingFieldError(ValidationError):
    """A required field is missing."""
    def __init__(self, field: str):
        super().__init__(f"Missing required field: {field}", field=field)
        self.code = "MISSING_FIELD"


class InvalidFormatError(ValidationError):
    """A field has an invalid format."""
    def __init__(self, field: str, expected: str):
        super().__init__(
            f"Invalid format for '{field}': expected {expected}",
            field=field, expected=expected,
        )
        self.code = "INVALID_FORMAT"


class AuthError(AppError):
    """Base for authentication/authorization failures."""
    def __init__(self, message: str, **context):
        super().__init__(message, code="AUTH_ERROR", **context)


class NotAuthenticatedError(AuthError):
    """User is not authenticated."""
    def __init__(self):
        super().__init__("Authentication required")
        self.code = "NOT_AUTHENTICATED"


class NotAuthorizedError(AuthError):
    """User lacks permission."""
    def __init__(self, action: str, resource: str):
        super().__init__(
            f"Not authorized to {action} on {resource}",
            action=action, resource=resource,
        )
        self.code = "NOT_AUTHORIZED"


class ExternalServiceError(AppError):
    """Base for external service failures."""
    def __init__(self, message: str, service: str, **context):
        super().__init__(message, code="EXTERNAL_ERROR", service=service, **context)


# ── Simulated request processing ──────────────────────────────

def process_request(request: dict) -> dict:
    # Validation
    if "email" not in request:
        raise MissingFieldError("email")
    if "@" not in request["email"]:
        raise InvalidFormatError("email", "user@domain.com")

    # Auth
    if "token" not in request:
        raise NotAuthenticatedError()
    if request.get("role") != "admin":
        raise NotAuthorizedError("create", "user")

    return {"status": "created", "email": request["email"]}


def handle_request(request: dict) -> tuple[int, str]:
    """Demonstrates catching at different hierarchy levels."""
    try:
        result = process_request(request)
        return (200, f"OK: {result}")
    except NotAuthenticatedError as e:
        return (401, f"[{e.code}] {e}")
    except NotAuthorizedError as e:
        return (403, f"[{e.code}] {e}")
    except ValidationError as e:
        # Catches MissingFieldError AND InvalidFormatError
        return (400, f"[{e.code}] {e}")
    except AppError as e:
        return (500, f"[{e.code}] Internal error")


# ── Tests ─────────────────────────────────────────────────────

def test_missing_field():
    status, body = handle_request({})
    assert status == 400 and "MISSING_FIELD" in body
    print(f"PASS: missing field -> {status} {body}")


def test_invalid_format():
    status, body = handle_request({"email": "bad"})
    assert status == 400 and "INVALID_FORMAT" in body
    print(f"PASS: invalid format -> {status} {body}")


def test_not_authenticated():
    status, body = handle_request({"email": "a@b.com"})
    assert status == 401
    print(f"PASS: not authenticated -> {status} {body}")


def test_not_authorized():
    status, body = handle_request({"email": "a@b.com", "token": "t", "role": "viewer"})
    assert status == 403
    print(f"PASS: not authorized -> {status} {body}")


def test_success():
    status, body = handle_request({"email": "a@b.com", "token": "t", "role": "admin"})
    assert status == 200
    print(f"PASS: success -> {status} {body}")


def test_hierarchy_catch_category():
    """Catching ValidationError catches both leaf types."""
    caught_types = []
    for req in [{}, {"email": "bad"}]:
        try:
            process_request(req)
        except ValidationError as e:
            caught_types.append(type(e).__name__)
    assert caught_types == ["MissingFieldError", "InvalidFormatError"]
    print(f"PASS: category catch got: {caught_types}")


if __name__ == "__main__":
    test_missing_field()
    test_invalid_format()
    test_not_authenticated()
    test_not_authorized()
    test_success()
    test_hierarchy_catch_category()
    print("\nAll Exception Hierarchy tests passed.")
```

## Go

```go
// exception_hierarchy.go — Go uses sentinel errors + custom types
package main

import (
	"errors"
	"fmt"
)

// ── Error hierarchy via custom types ─────────────────────────

type AppError struct {
	Code    string
	Message string
}

func (e *AppError) Error() string {
	return fmt.Sprintf("[%s] %s", e.Code, e.Message)
}

type ValidationError struct {
	AppError
	Field string
}

type MissingFieldError struct {
	ValidationError
}

func (e *MissingFieldError) Unwrap() error { return &e.ValidationError }

func NewMissingFieldError(field string) *MissingFieldError {
	return &MissingFieldError{
		ValidationError: ValidationError{
			AppError: AppError{Code: "MISSING_FIELD", Message: "Missing required field: " + field},
			Field:    field,
		},
	}
}

type InvalidFormatError struct {
	ValidationError
	Expected string
}

func (e *InvalidFormatError) Unwrap() error { return &e.ValidationError }

func NewInvalidFormatError(field, expected string) *InvalidFormatError {
	return &InvalidFormatError{
		ValidationError: ValidationError{
			AppError: AppError{
				Code:    "INVALID_FORMAT",
				Message: fmt.Sprintf("Invalid format for '%s': expected %s", field, expected),
			},
			Field: field,
		},
		Expected: expected,
	}
}

type AuthError struct {
	AppError
}

type NotAuthenticatedError struct {
	AuthError
}

func NewNotAuthenticatedError() *NotAuthenticatedError {
	return &NotAuthenticatedError{
		AuthError: AuthError{
			AppError: AppError{Code: "NOT_AUTHENTICATED", Message: "Authentication required"},
		},
	}
}

type NotAuthorizedError struct {
	AuthError
	Action   string
	Resource string
}

func NewNotAuthorizedError(action, resource string) *NotAuthorizedError {
	return &NotAuthorizedError{
		AuthError: AuthError{
			AppError: AppError{
				Code:    "NOT_AUTHORIZED",
				Message: fmt.Sprintf("Not authorized to %s on %s", action, resource),
			},
		},
		Action:   action,
		Resource: resource,
	}
}

// ── Request processing ───────────────────────────────────────

type Request struct {
	Email string
	Token string
	Role  string
}

func processRequest(req Request) (string, error) {
	if req.Email == "" {
		return "", NewMissingFieldError("email")
	}
	if len(req.Email) < 3 || req.Email[0] == '@' {
		return "", NewInvalidFormatError("email", "user@domain.com")
	}
	if req.Token == "" {
		return "", NewNotAuthenticatedError()
	}
	if req.Role != "admin" {
		return "", NewNotAuthorizedError("create", "user")
	}
	return fmt.Sprintf("created: %s", req.Email), nil
}

func handleRequest(req Request) (int, string) {
	result, err := processRequest(req)
	if err == nil {
		return 200, fmt.Sprintf("OK: %s", result)
	}

	// Check hierarchy — more specific types first via errors.As
	var missingField *MissingFieldError
	if errors.As(err, &missingField) {
		return 400, err.Error()
	}
	var invalidFmt *InvalidFormatError
	if errors.As(err, &invalidFmt) {
		return 400, err.Error()
	}
	var notAuth *NotAuthenticatedError
	if errors.As(err, &notAuth) {
		return 401, err.Error()
	}
	var notAuthz *NotAuthorizedError
	if errors.As(err, &notAuthz) {
		return 403, err.Error()
	}
	return 500, "internal error"
}

// ── Tests ────────────────────────────────────────────────────

func testMissingField() {
	status, body := handleRequest(Request{})
	if status != 400 {
		panic("expected 400")
	}
	fmt.Printf("PASS: missing field -> %d %s\n", status, body)
}

func testNotAuthenticated() {
	status, body := handleRequest(Request{Email: "a@b.com"})
	if status != 401 {
		panic("expected 401")
	}
	fmt.Printf("PASS: not authenticated -> %d %s\n", status, body)
}

func testNotAuthorized() {
	status, body := handleRequest(Request{Email: "a@b.com", Token: "t", Role: "viewer"})
	if status != 403 {
		panic("expected 403")
	}
	fmt.Printf("PASS: not authorized -> %d %s\n", status, body)
}

func testSuccess() {
	status, body := handleRequest(Request{Email: "a@b.com", Token: "t", Role: "admin"})
	if status != 200 {
		panic("expected 200")
	}
	fmt.Printf("PASS: success -> %d %s\n", status, body)
}

func testErrorsAsHierarchy() {
	err := NewMissingFieldError("email")
	var valErr *ValidationError
	if !errors.As(err, &valErr) {
		panic("MissingFieldError should be catchable as ValidationError")
	}
	fmt.Printf("PASS: errors.As hierarchy works: MissingFieldError -> ValidationError\n")
}

func main() {
	testMissingField()
	testNotAuthenticated()
	testNotAuthorized()
	testSuccess()
	testErrorsAsHierarchy()
	fmt.Println("\nAll Exception Hierarchy tests passed.")
}
```

## JavaScript

```javascript
// exception_hierarchy.js

// ── Exception hierarchy ──────────────────────────────────────

class AppError extends Error {
  constructor(message, code = "APP_ERROR") {
    super(message);
    this.name = this.constructor.name;
    this.code = code;
  }
}

class ValidationError extends AppError {
  constructor(message, field) {
    super(message, "VALIDATION_ERROR");
    this.field = field;
  }
}

class MissingFieldError extends ValidationError {
  constructor(field) {
    super(`Missing required field: ${field}`, field);
    this.code = "MISSING_FIELD";
  }
}

class InvalidFormatError extends ValidationError {
  constructor(field, expected) {
    super(`Invalid format for '${field}': expected ${expected}`, field);
    this.code = "INVALID_FORMAT";
    this.expected = expected;
  }
}

class AuthError extends AppError {
  constructor(message, code = "AUTH_ERROR") {
    super(message, code);
  }
}

class NotAuthenticatedError extends AuthError {
  constructor() {
    super("Authentication required", "NOT_AUTHENTICATED");
  }
}

class NotAuthorizedError extends AuthError {
  constructor(action, resource) {
    super(`Not authorized to ${action} on ${resource}`, "NOT_AUTHORIZED");
    this.action = action;
    this.resource = resource;
  }
}

// ── Request processing ───────────────────────────────────────

function processRequest(req) {
  if (!req.email) throw new MissingFieldError("email");
  if (!req.email.includes("@"))
    throw new InvalidFormatError("email", "user@domain.com");
  if (!req.token) throw new NotAuthenticatedError();
  if (req.role !== "admin") throw new NotAuthorizedError("create", "user");
  return { status: "created", email: req.email };
}

function handleRequest(req) {
  try {
    const result = processRequest(req);
    return { status: 200, body: `OK: ${JSON.stringify(result)}` };
  } catch (e) {
    if (e instanceof NotAuthenticatedError) {
      return { status: 401, body: `[${e.code}] ${e.message}` };
    }
    if (e instanceof NotAuthorizedError) {
      return { status: 403, body: `[${e.code}] ${e.message}` };
    }
    if (e instanceof ValidationError) {
      return { status: 400, body: `[${e.code}] ${e.message}` };
    }
    if (e instanceof AppError) {
      return { status: 500, body: `[${e.code}] Internal error` };
    }
    throw e;
  }
}

// ── Tests ────────────────────────────────────────────────────

function testMissingField() {
  const { status, body } = handleRequest({});
  console.assert(status === 400 && body.includes("MISSING_FIELD"));
  console.log(`PASS: missing field -> ${status} ${body}`);
}

function testInvalidFormat() {
  const { status, body } = handleRequest({ email: "bad" });
  console.assert(status === 400 && body.includes("INVALID_FORMAT"));
  console.log(`PASS: invalid format -> ${status} ${body}`);
}

function testNotAuthenticated() {
  const { status, body } = handleRequest({ email: "a@b.com" });
  console.assert(status === 401);
  console.log(`PASS: not authenticated -> ${status} ${body}`);
}

function testNotAuthorized() {
  const { status, body } = handleRequest({
    email: "a@b.com",
    token: "t",
    role: "viewer",
  });
  console.assert(status === 403);
  console.log(`PASS: not authorized -> ${status} ${body}`);
}

function testSuccess() {
  const { status, body } = handleRequest({
    email: "a@b.com",
    token: "t",
    role: "admin",
  });
  console.assert(status === 200);
  console.log(`PASS: success -> ${status} ${body}`);
}

function testInstanceofHierarchy() {
  const err = new MissingFieldError("email");
  console.assert(err instanceof ValidationError);
  console.assert(err instanceof AppError);
  console.assert(err instanceof Error);
  console.log(
    `PASS: instanceof chain: MissingFieldError -> ValidationError -> AppError -> Error`
  );
}

testMissingField();
testInvalidFormat();
testNotAuthenticated();
testNotAuthorized();
testSuccess();
testInstanceofHierarchy();
console.log("\nAll Exception Hierarchy tests passed.");
```

## Related Patterns

- **Chain of Responsibility** — exception handling catch blocks form a chain; each handler catches its type and passes upward if unhandled.
- **Observer** — error logging and alerting are observer-based: error events are published and logging subscribers handle them.
- **Guard Clause** — guard clauses prevent exceptions from being thrown in the first place by validating early.
