hunter-party-py/error-hunter-py/SKILL.md
Audit Python code for error handling design quality — missing exception hierarchies, lost exception chains, return-None antipatterns, over-broad try/except, silent suppression, poor exception context, framework handler gaps, and missing error boundaries. Use when: reviewing error handling strategy, tightening exception design before deployment, auditing error propagation paths, standardizing API error responses, or establishing error handling conventions after rapid feature development.
npx skillsauth add skyosev/agent-skills error-hunter-pyInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Audit Python code for error handling design quality — whether exceptions are structured intentionally, propagated correctly, caught at the right boundaries, and converted to appropriate responses. The goal: exceptions are part of the API contract, error boundaries are clearly defined, and every except block justifies its existence.
This skill focuses on error handling design — the strategy of how errors are raised, propagated, caught, and
converted. For type-system enforcement of invariants (Any, cast(), # type: ignore), see invariant-hunter-py.
For security implications of error patterns (stack trace leakage, auth bypass on error), see security-hunter-py.
Exceptions are part of the API. The exceptions a function raises are as much part of its contract as its return type. Document them, type them, and design them intentionally.
Hierarchy mirrors the domain. Exception classes should reflect domain concepts (OrderNotFoundError,
PaymentDeclinedError), not technical categories (DatabaseError leaking from domain). Technical exceptions belong
at the infrastructure boundary.
Chain, don't replace. When wrapping an exception, always use raise X from Y. The original traceback is the
most valuable debugging information — losing it is a bug.
Raise, don't return None. Functions should raise on failure and return values on success. Optional return
types are for genuinely absent data (not-found is a valid domain outcome), not for errors. When explicit error paths
are needed, consider a Result type pattern.
Fail loudly in non-boundary code. Business logic and domain code should let exceptions propagate. Only catch at defined boundaries: API handlers, middleware, background task runners, CLI entry points.
Every except block must justify its existence. A catch is justified if it: (a) recovers meaningfully, (b) wraps and re-raises at a boundary, or (c) converts to an appropriate error response. Catch-log-swallow is almost never justified.
Standardize error responses. API error shapes should be consistent across the entire application. Pick one
format (RFC 7807 Problem Details, or a simple {"type", "title", "detail", "status"} shape) and use it everywhere.
Not every finding requires action. Document these but do not flag as "must-fix":
| Pattern | When Acceptable |
| ------- | --------------- |
| except Exception at API boundary | Top-level handler converting to 500 response |
| return None for not-found | Repository .find_by_id() where absence is a normal outcome |
| Catch-and-log without re-raise | Background task runners with defined retry logic |
| Generic ValueError | Input validation in pure utility functions with no domain context |
| except Exception in test helpers | Test utilities where failure mode doesn't matter |
Flat raise Exception("...") instead of domain-specific exception classes. No base ApplicationError. Exception names
without Error suffix.
Signals:
raise Exception( used for domain-specific error conditionsraise ValueError("generic message") used for domain errors instead of domain-specific exceptionsError suffix (inconsistent naming convention)ApplicationError or DomainError for the project's exception hierarchyAction: Create a domain exception hierarchy: ApplicationError → NotFoundError, ValidationError,
AuthorizationError, etc. Use Error suffix consistently. Keep the hierarchy shallow (2-3 levels max).
raise X inside except Y without from Y, losing the original traceback and context.
Signals:
except SomeError: raise OtherError(...) without fromraise followed by a different exception type with no chainingfrom original_errorstr(e) because the chain was lost upstreamAction: Add from original_error to preserve the exception chain. Use raise X from None only when deliberately
suppressing the chain (rare — document the reason).
Functions returning None on error instead of raising, conflating "not found" with "error" with "no result."
Signals:
return None inside except blocks-> Optional[X] where None means both "not found" and "error"if result is None without knowing why the result is absentNone on infrastructure failures (DB connection error, timeout) instead of raisingNone on some errors and raises on othersAction: Raise specific exceptions for errors. Use Optional only for genuinely absent data (e.g., repository
find_by_id returning None for a missing record). Consider Result type patterns for explicit error paths where
exceptions are too heavy.
Try blocks wrapping too many statements when only one can raise the caught exception. Catching Exception when a
specific type is appropriate.
Signals:
try: blocks spanning 10+ linesexcept Exception: in non-boundary codeexcept Exception as e: pass or except Exception as e: log(e) without recoveryException where ValueError, KeyError, TypeError etc. would be specific enoughAction: Narrow try blocks to the minimum statements that can raise. Catch specific exception types. Keep
except Exception only at defined error boundaries (top-level handlers, middleware).
Bare except:, except Exception: pass, catch-and-log-only patterns that swallow errors without recovery or re-raise.
Signals:
except: (bare)except Exception: passexcept Exception as e: logger.error(str(e)) with no re-raise and no recovery logicreturn [] or return {} or return None as silent fallbacks in except blockscontinue in except blocks inside loops, silently skipping failed items without reportingAction: Either recover meaningfully or re-raise. At error boundaries (middleware, top-level handlers), catch and convert to appropriate response. Elsewhere, let exceptions propagate.
Exceptions with bare string messages lacking relevant data attributes, or generic messages that don't help diagnose the issue.
Signals:
raise ValueError("invalid input") without saying which input or what constraint was violatedstr(e) in log messages instead of logger.exception()order_id, field_name, etc. on the exception)Action: Add context attributes to exception classes (order_id, field_name, etc.). Use
logger.exception("msg") to capture traceback. Include the invalid value and expected constraint in error messages.
Design exception classes with __init__ that accepts structured data, not just a message string.
Missing or inconsistent mapping between domain exceptions and HTTP/API responses. Exception handlers that leak internal details.
Signals:
@app.exception_handler() (FastAPI) or @app.errorhandler() (Flask) registrations for domain exceptions500 for all domain errors instead of appropriate status codes{"error": ...} vs {"detail": ...} vs {"message": ...})Action: Register exception handlers for each domain exception type. Standardize error response shape. Strip
internal details in production. Map domain exceptions to appropriate HTTP status codes (e.g., NotFoundError → 404,
ValidationError → 422, AuthorizationError → 403).
No clear separation between where errors should propagate and where they should be caught and converted. Error handling scattered without a strategy.
Signals:
except SQLAlchemyError in domain layer)Action: Define explicit error boundaries: API boundary (convert to HTTP response), service boundary (convert infrastructure errors to domain errors), infrastructure boundary (wrap external errors). Let exceptions propagate through layers where no conversion is needed.
Resolve audit surface. The prompt may specify the scope as:
main/master)BASE=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@' || echo main)
SCOPE=$(git diff --name-only $(git merge-base HEAD $BASE)...HEAD)
Constrain all subsequent scans to the resolved surface.
Identify error boundaries: API route handlers, middleware, CLI entry points, background task runners, event listeners, webhook handlers. These are the places where exceptions should be caught and converted.
Identify the current exception hierarchy: custom exception classes, base classes, naming conventions.
EXCLUDE='--glob !**/*_test.py --glob !**/test_*.py --glob !**/tests/** --glob !**/venv/** --glob !**/.venv/**'
# Exception hierarchy
rg 'class\s+\w+(Error|Exception)\s*\(' --type py $EXCLUDE
rg 'raise\s+Exception\(' --type py $EXCLUDE
rg 'raise\s+ValueError\(' --type py $EXCLUDE
# Missing chaining
rg -U 'except\s+\w+.*:\s*\n\s*raise\s+\w+' --type py $EXCLUDE
# Return-None in except
rg -U 'except.*:\s*\n\s*return None' --type py $EXCLUDE
# Broad try/except
rg --pcre2 'except\s*:' --type py $EXCLUDE
rg --pcre2 'except\s+Exception\s*:' --type py $EXCLUDE
rg --pcre2 'except\s+Exception\s+as\s+\w+\s*:\s*$' --type py $EXCLUDE
# Silent suppression
rg -U 'except.*:\s*\n\s*pass' --type py $EXCLUDE
# Error handler registrations
rg 'exception_handler|errorhandler|ExceptionMiddleware' --type py $EXCLUDE
# Logging patterns
rg 'logger\.(error|warning).*str\(.*\)' --type py $EXCLUDE
rg 'logger\.exception' --type py $EXCLUDE
ApplicationError? Do domain exceptions extend it? Is the hierarchy shallow
(2-3 levels) or excessively deep?Error suffix? Are names domain-specific
(OrderNotFoundError) or generic (CustomError)?order_id, field_name) or just string
messages?For each domain error, trace from raise site to final handling:
from? Is context added?Flag exceptions that: propagate unhandled to a generic 500, are caught and swallowed mid-stack, or lose their chain during wrapping.
SQLAlchemyError reaching the API handler.)Save as YYYY-MM-DD-error-hunter-audit-{$LLM-name}.md in the project's docs folder (or project root if no docs
folder exists).
# Error Hunter Audit — {date}
## Scope
- Surface: {diff / path / codebase}
- Files: {count or list}
- Exclusions: {list}
## Error Boundary Map
| # | Boundary | Location | Type | Handles |
| - | -------- | -------- | ---- | ------- |
| 1 | API middleware | file:line | Top-level | All unhandled → 500 |
| 2 | POST /api/orders | file:line | Endpoint | OrderNotFound → 404 |
## Exception Hierarchy
{Current hierarchy tree, or "No custom exceptions found"}
## Findings
### Missing Exception Hierarchy
| # | Location | Current | Suggested | Severity |
| - | -------- | ------- | --------- | -------- |
| 1 | file:line | `raise Exception("Order not found")` | `raise OrderNotFoundError(order_id)` | High |
### Missing Exception Chaining
| # | Location | Outer Exception | Inner Exception | Action |
| - | -------- | --------------- | --------------- | ------ |
| 1 | file:line | `raise ServiceError(...)` | `except DBError` | Add `from e` |
### Return-None Antipattern
| # | Location | Function | Returns None When | Action |
| - | -------- | -------- | ----------------- | ------ |
| 1 | file:line | `get_order()` | DB error + not found (conflated) | Split: raise on error, None on not-found |
### Over-Broad Try/Except
| # | Location | Try Block Lines | Caught Type | Action |
| - | -------- | --------------- | ----------- | ------ |
| 1 | file:line | 15 | `Exception` | Narrow to 2 lines, catch `ValueError` |
### Silent Error Suppression
| # | Location | Pattern | Action |
| - | -------- | ------- | ------ |
| 1 | file:line | `except Exception: pass` | Remove or add recovery logic |
### Poor Exception Context
| # | Location | Exception | Missing Context | Action |
| - | -------- | --------- | --------------- | ------ |
| 1 | file:line | `ValueError("invalid")` | Which value? What constraint? | Include value and expected range |
### Framework Error Handler Gaps
| # | Exception Type | Expected Status | Current Handling | Action |
| - | -------------- | --------------- | ---------------- | ------ |
| 1 | `OrderNotFoundError` | 404 | Unhandled (500) | Register exception handler |
### Missing Error Boundaries
| # | Location | Issue | Action |
| - | -------- | ----- | ------ |
| 1 | file:line | Domain code catches `SQLAlchemyError` directly | Wrap in repository layer |
## Recommendations (Priority Order)
1. **Must-fix**: {silent suppression, missing chaining, unhandled domain exceptions at API boundary}
2. **Should-fix**: {missing hierarchy, return-None antipattern, over-broad try/except}
3. **Consider**: {error response standardization, Result type adoption, poor exception context}
Any, cast(),
# type: ignore) and loose optionality from a type-enforcement perspective. Error-hunter owns how exceptions are
structured, propagated, caught, and converted — the design of the error handling strategy. If the finding is about
a bare except: suppressing an invariant, it belongs here. If the finding is about # type: ignore without
justification, it belongs in invariant-hunter.file/path.py:line with the exact code.development
Transforms vague feature ideas into precise, codebase-grounded technical requirements. Use when requirements are ambiguous/incomplete, the user struggles to describe behavior, terminology is unclear, or multiple concepts are mixed. Output is a requirements spec—NOT an implementation plan.
tools
Audit TypeScript type definitions for design debt — duplicated shapes, missing derivations, over-engineered generics, under-constrained type parameters, reinvented utility types, and disorganized type architecture. Type structure and maintainability, not type enforcement. Use when: reviewing type definitions for maintainability, reducing type duplication, simplifying over-engineered type-level logic, or reorganizing type architecture after growth.
development
Audit TypeScript test code for quality gaps — missing coverage on critical paths, brittle tests coupled to implementation, over-mocking, assertion-free tests, missing edge cases, and duplicated test setup. Focuses on test effectiveness, not production code structure. Use when: reviewing TypeScript test suites for reliability, reducing false-positive test failures, improving coverage of critical business logic, or cleaning up test debt.
tools
Audit TypeScript class and interface design for SOLID violations — god classes, rigid extension points, broken substitutability, fat interfaces, and concrete dependency chains. Focuses on responsibility assignment and abstraction fitness. Use when: reviewing class hierarchies, preparing for extension with new variants, reducing coupling between services, or improving testability of class-heavy code.