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.
184 lines
4.1 KiB
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")
|
|
```
|
|
|