# Error Handling Constraints ## Core Rule **Catch `Exception`, return safe fallbacks (False/[]/None)** Never let exceptions propagate to user-facing code. Always provide a safe default. ## Patterns ### For Not-Found Operations Return `None` or falsy value when item not found: ```python # GOOD: Return None on not found def get_motion_by_id(self, motion_id: int) -> Optional[Dict]: try: conn = duckdb.connect(self.db_path) result = conn.execute( "SELECT * FROM motions WHERE id = ?", (motion_id,) ).fetchone() conn.close() return result except Exception: conn.close() return None ``` ### For Collection Operations Return empty list when no results: ```python # GOOD: Return empty list on failure def get_filtered_motions(self, **kwargs) -> List[Dict]: try: conn = duckdb.connect(self.db_path) rows = conn.execute(query, params).fetchall() conn.close() return rows except Exception: conn.close() return [] ``` ### For Boolean Operations Return `False` for failed boolean checks: ```python # GOOD: Return False on failure def motion_exists(self, motion_id: int) -> bool: try: conn = duckdb.connect(self.db_path) count = conn.execute( "SELECT COUNT(*) FROM motions WHERE id = ?", (motion_id,) ).fetchone()[0] conn.close() return count > 0 except Exception: return False ``` ### For Creation Operations Return `False` or empty string on failure: ```python # GOOD: Return empty string on failure def generate_summary(self, title: str, body: str) -> str: try: return ai_provider.chat_completion(messages) except ai_provider.ProviderError: logger.exception("AI provider failed") return "" ``` ## Anti-Patterns to Avoid ### Don't Catch Specific Exceptions Only ```python # BAD: Catches only FileNotFoundError, misses other issues try: with open(path) as f: return json.load(f) except FileNotFoundError: return None ``` ### Don't Re-raise Without Context ```python # BAD: Loses information try: process(data) except Exception: raise # No context added ``` ### Don't Swallow Exceptions Silently ```python # BAD: No logging, no fallback try: return risky_operation() except Exception: pass # What happened? ``` ## Nested Exception Handling When calling code that has its own error handling, wrap only if needed: ```python # Accept result from wrapped function (it handles errors) def fetch_motions(self, start_date): # ai_provider_wrapper handles retries internally embeddings = get_embeddings_with_retry(texts) # Only wrap if wrapper doesn't handle errors if all(e is None for e in embeddings): logger.error("All embeddings failed") return [] return process(embeddings) ``` ## Context Managers Use `try/finally` for cleanup: ```python def process_with_temp_file(self): temp = NamedTemporaryFile(delete=False) try: temp.write(data) temp.close() return process_file(temp.name) finally: os.unlink(temp.name) temp.close() ``` ## When to Log vs Return | Scenario | Action | |----------|--------| | User action fails | Log warning, return safe default | | Internal error (corrupt data) | Log error, return safe default | | Transient failure (network) | Log warning, retry if appropriate | | Configuration error | Log error, raise with clear message | ## Exception Propagation Only raise exceptions for: 1. Configuration/setup errors (missing required env vars) 2. Programming errors (invalid arguments) 3. Fatal system errors (database corruption) ```python # GOOD: Raise for configuration errors def _get_api_key(self) -> str: key = os.environ.get("OPENROUTER_API_KEY") if not key: raise ProviderError( "OPENROUTER_API_KEY environment variable is required" ) return key ``` ## Logging Errors Always include context: ```python # GOOD: Include relevant context _logger.error( "Failed to fetch motion %d: %s", motion_id, exc ) # BAD: No context _logger.error("Failed to fetch") ```