You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
motief/.mindmodel/constraints/error-handling.yaml

184 lines
4.1 KiB

# 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")
```