Initial commit
This commit is contained in:
340
.claude/skills/error-handling-completeness/SKILL.md
Normal file
340
.claude/skills/error-handling-completeness/SKILL.md
Normal file
@@ -0,0 +1,340 @@
|
||||
---
|
||||
name: error-handling-completeness
|
||||
description: Evaluates if error handling is sufficient for new code - checks try-catch coverage, logging, user messages, retry logic. Focuses on external calls and user-facing code.
|
||||
---
|
||||
|
||||
# Error Handling Completeness Skill
|
||||
|
||||
**Purpose**: Prevent production crashes with systematic error handling.
|
||||
|
||||
**Trigger Words**: API call, external, integration, network, database, file, user input, async, promise, await
|
||||
|
||||
---
|
||||
|
||||
## Quick Decision: Needs Error Handling Check?
|
||||
|
||||
```python
|
||||
def needs_error_check(code_context: dict) -> bool:
|
||||
"""Decide if error handling review is needed."""
|
||||
|
||||
# High-risk operations (always check)
|
||||
high_risk = [
|
||||
"fetch", "axios", "requests", "http", # HTTP calls
|
||||
"db.", "query", "execute", # Database
|
||||
"open(", "read", "write", # File I/O
|
||||
"json.loads", "json.parse", # JSON parsing
|
||||
"int(", "float(", # Type conversions
|
||||
"subprocess", "exec", # External processes
|
||||
"await", "async", # Async operations
|
||||
]
|
||||
|
||||
code = code_context.get("code", "").lower()
|
||||
return any(risk in code for risk in high_risk)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling Checklist (Fast)
|
||||
|
||||
### 1. **External API Calls** (Most Critical)
|
||||
```python
|
||||
# ❌ BAD - No error handling
|
||||
def get_user_data(user_id):
|
||||
response = requests.get(f"https://api.example.com/users/{user_id}")
|
||||
return response.json() # What if network fails? 404? Timeout?
|
||||
|
||||
# ✅ GOOD - Complete error handling
|
||||
def get_user_data(user_id):
|
||||
try:
|
||||
response = requests.get(
|
||||
f"https://api.example.com/users/{user_id}",
|
||||
timeout=5 # Timeout!
|
||||
)
|
||||
response.raise_for_status() # Check HTTP errors
|
||||
return response.json()
|
||||
|
||||
except requests.Timeout:
|
||||
logger.error(f"Timeout fetching user {user_id}")
|
||||
raise ServiceUnavailableError("User service timeout")
|
||||
|
||||
except requests.HTTPError as e:
|
||||
if e.response.status_code == 404:
|
||||
raise UserNotFoundError(f"User {user_id} not found")
|
||||
logger.error(f"HTTP error fetching user: {e}")
|
||||
raise
|
||||
|
||||
except requests.RequestException as e:
|
||||
logger.error(f"Network error: {e}")
|
||||
raise ServiceUnavailableError("Cannot reach user service")
|
||||
```
|
||||
|
||||
**Quick Checks**:
|
||||
- ✅ Timeout set?
|
||||
- ✅ HTTP errors handled?
|
||||
- ✅ Network errors caught?
|
||||
- ✅ Logged?
|
||||
- ✅ User-friendly error returned?
|
||||
|
||||
---
|
||||
|
||||
### 2. **Database Operations**
|
||||
```python
|
||||
# ❌ BAD - Swallows errors
|
||||
def delete_user(user_id):
|
||||
try:
|
||||
db.execute("DELETE FROM users WHERE id = ?", [user_id])
|
||||
except Exception:
|
||||
pass # Silent failure!
|
||||
|
||||
# ✅ GOOD - Specific handling
|
||||
def delete_user(user_id):
|
||||
try:
|
||||
result = db.execute("DELETE FROM users WHERE id = ?", [user_id])
|
||||
if result.rowcount == 0:
|
||||
raise UserNotFoundError(f"User {user_id} not found")
|
||||
|
||||
except db.IntegrityError as e:
|
||||
logger.error(f"Cannot delete user {user_id}: {e}")
|
||||
raise DependencyError("User has related records")
|
||||
|
||||
except db.OperationalError as e:
|
||||
logger.error(f"Database error: {e}")
|
||||
raise DatabaseUnavailableError()
|
||||
```
|
||||
|
||||
**Quick Checks**:
|
||||
- ✅ Specific exceptions (not bare `except`)?
|
||||
- ✅ Logged?
|
||||
- ✅ User-friendly error?
|
||||
|
||||
---
|
||||
|
||||
### 3. **File Operations**
|
||||
```python
|
||||
# ❌ BAD - File might not exist
|
||||
def read_config():
|
||||
with open("config.json") as f:
|
||||
return json.load(f)
|
||||
|
||||
# ✅ GOOD - Handle missing file
|
||||
def read_config():
|
||||
try:
|
||||
with open("config.json") as f:
|
||||
return json.load(f)
|
||||
except FileNotFoundError:
|
||||
logger.warning("config.json not found, using defaults")
|
||||
return DEFAULT_CONFIG
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"Invalid JSON in config.json: {e}")
|
||||
raise ConfigurationError("Malformed config.json")
|
||||
except PermissionError:
|
||||
logger.error("Permission denied reading config.json")
|
||||
raise
|
||||
```
|
||||
|
||||
**Quick Checks**:
|
||||
- ✅ FileNotFoundError handled?
|
||||
- ✅ JSON parse errors caught?
|
||||
- ✅ Permission errors handled?
|
||||
|
||||
---
|
||||
|
||||
### 4. **Type Conversions**
|
||||
```python
|
||||
# ❌ BAD - Crash on invalid input
|
||||
def process_age(age_str):
|
||||
age = int(age_str) # What if "abc"?
|
||||
return age * 2
|
||||
|
||||
# ✅ GOOD - Validated
|
||||
def process_age(age_str):
|
||||
try:
|
||||
age = int(age_str)
|
||||
if age < 0 or age > 150:
|
||||
raise ValueError("Age out of range")
|
||||
return age * 2
|
||||
except ValueError:
|
||||
raise ValidationError(f"Invalid age: {age_str}")
|
||||
```
|
||||
|
||||
**Quick Checks**:
|
||||
- ✅ ValueError caught?
|
||||
- ✅ Range validation?
|
||||
- ✅ Clear error message?
|
||||
|
||||
---
|
||||
|
||||
### 5. **Async/Await** (JavaScript/Python)
|
||||
```javascript
|
||||
// ❌ BAD - Unhandled promise rejection
|
||||
async function fetchUser(id) {
|
||||
const user = await fetch(`/api/users/${id}`);
|
||||
return user.json(); // What if network fails?
|
||||
}
|
||||
|
||||
// ✅ GOOD - Handled
|
||||
async function fetchUser(id) {
|
||||
try {
|
||||
const response = await fetch(`/api/users/${id}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch user ${id}:`, error);
|
||||
throw new ServiceError("Cannot fetch user");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Quick Checks**:
|
||||
- ✅ Try-catch around await?
|
||||
- ✅ HTTP status checked?
|
||||
- ✅ Logged?
|
||||
|
||||
---
|
||||
|
||||
## Error Handling Patterns
|
||||
|
||||
### Pattern 1: Retry with Exponential Backoff
|
||||
```python
|
||||
def call_api_with_retry(url, max_retries=3):
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
response = requests.get(url, timeout=5)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
except requests.Timeout:
|
||||
if attempt < max_retries - 1:
|
||||
wait = 2 ** attempt # 1s, 2s, 4s
|
||||
logger.warning(f"Timeout, retrying in {wait}s...")
|
||||
time.sleep(wait)
|
||||
else:
|
||||
raise
|
||||
```
|
||||
|
||||
**When to use**: Transient failures (network, rate limits)
|
||||
|
||||
---
|
||||
|
||||
### Pattern 2: Fallback Values
|
||||
```python
|
||||
def get_user_avatar(user_id):
|
||||
try:
|
||||
return fetch_from_cdn(user_id)
|
||||
except CDNError:
|
||||
logger.warning(f"CDN failed for user {user_id}, using default")
|
||||
return DEFAULT_AVATAR_URL
|
||||
```
|
||||
|
||||
**When to use**: Non-critical operations, graceful degradation
|
||||
|
||||
---
|
||||
|
||||
### Pattern 3: Circuit Breaker
|
||||
```python
|
||||
class CircuitBreaker:
|
||||
def __init__(self, max_failures=5):
|
||||
self.failures = 0
|
||||
self.max_failures = max_failures
|
||||
self.is_open = False
|
||||
|
||||
def call(self, func):
|
||||
if self.is_open:
|
||||
raise ServiceUnavailableError("Circuit breaker open")
|
||||
|
||||
try:
|
||||
result = func()
|
||||
self.failures = 0 # Reset on success
|
||||
return result
|
||||
except Exception as e:
|
||||
self.failures += 1
|
||||
if self.failures >= self.max_failures:
|
||||
self.is_open = True
|
||||
logger.error("Circuit breaker opened")
|
||||
raise
|
||||
```
|
||||
|
||||
**When to use**: Preventing cascading failures
|
||||
|
||||
---
|
||||
|
||||
## Output Format
|
||||
|
||||
```markdown
|
||||
## Error Handling Report
|
||||
|
||||
**Status**: [✅ COMPLETE | ⚠️ GAPS FOUND]
|
||||
|
||||
---
|
||||
|
||||
### Missing Error Handling: 3
|
||||
|
||||
1. **[HIGH] No timeout on API call (api_client.py:45)**
|
||||
- **Issue**: `requests.get()` has no timeout
|
||||
- **Risk**: Indefinite hang if service slow
|
||||
- **Fix**:
|
||||
```python
|
||||
response = requests.get(url, timeout=5)
|
||||
```
|
||||
|
||||
2. **[HIGH] Unhandled JSON parse error (config.py:12)**
|
||||
- **Issue**: `json.load()` not wrapped in try-catch
|
||||
- **Risk**: Crash on malformed JSON
|
||||
- **Fix**:
|
||||
```python
|
||||
try:
|
||||
config = json.load(f)
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"Invalid JSON: {e}")
|
||||
return DEFAULT_CONFIG
|
||||
```
|
||||
|
||||
3. **[MEDIUM] Silent exception swallowing (db.py:89)**
|
||||
- **Issue**: `except Exception: pass`
|
||||
- **Risk**: Failures go unnoticed
|
||||
- **Fix**: Log error or use specific exception
|
||||
|
||||
---
|
||||
|
||||
**Good Practices Found**: 2
|
||||
- ✅ Database errors logged properly (db.py:34)
|
||||
- ✅ Retry logic on payment API (payments.py:67)
|
||||
|
||||
---
|
||||
|
||||
**Next Steps**:
|
||||
1. Add timeout to API calls (5 min)
|
||||
2. Wrap JSON parsing in try-catch (2 min)
|
||||
3. Remove silent exception handlers (3 min)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## What This Skill Does NOT Do
|
||||
|
||||
❌ Catch every possible exception (too noisy)
|
||||
❌ Force try-catch everywhere (only where needed)
|
||||
❌ Replace integration tests
|
||||
❌ Handle business logic errors (validation, etc.)
|
||||
|
||||
✅ **DOES**: Check critical error-prone operations (network, I/O, parsing)
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
```bash
|
||||
# Strict mode: check all functions
|
||||
export LAZYDEV_ERROR_HANDLING_STRICT=1
|
||||
|
||||
# Disable error handling checks
|
||||
export LAZYDEV_DISABLE_ERROR_CHECKS=1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Version**: 1.0.0
|
||||
**Focus**: External calls, I/O, parsing, async
|
||||
**Speed**: <2 seconds per file
|
||||
Reference in New Issue
Block a user