Error Handling Analysis
Debtmap provides comprehensive error handling analysis across all supported languages (Rust, Python, JavaScript, TypeScript), detecting anti-patterns that lead to silent failures, production panics, and difficult-to-debug issues.
Overview
Error handling issues are classified as ErrorSwallowing debt with Major severity (weight 4), reflecting their significant impact on code reliability and debuggability. Debtmap detects:
- Error swallowing: Exception handlers that silently catch errors without logging or re-raising
- Panic patterns: Rust code that can panic in production (unwrap, expect, panic!)
- Error propagation issues: Missing error context in Result chains
- Async error handling: Unhandled promise rejections, dropped futures, missing await
- Python-specific patterns: Bare except clauses, silent exception handling
All error handling patterns are filtered intelligently - code detected in test modules (e.g., #[cfg(test)], test_ prefixes) receives lower priority or is excluded entirely.
Rust Error Handling Analysis
Panic Pattern Detection
Debtmap identifies Rust code that can panic at runtime instead of returning Result:
Detected patterns:
#![allow(unused)] fn main() { // ❌ CRITICAL: Direct panic in production code fn process_data(value: Option<i32>) -> i32 { panic!("not implemented"); // Detected: PanicInNonTest } // ❌ HIGH: Unwrap on Result fn read_config(path: &Path) -> Config { let content = fs::read_to_string(path).unwrap(); // Detected: UnwrapOnResult parse_config(&content) } // ❌ HIGH: Unwrap on Option fn get_user(id: u32) -> User { users.get(&id).unwrap() // Detected: UnwrapOnOption } // ❌ MEDIUM: Expect with generic message fn parse_value(s: &str) -> i32 { s.parse().expect("parse failed") // Detected: ExpectWithGenericMessage } // ❌ MEDIUM: TODO in production fn calculate_tax(amount: f64) -> f64 { todo!("implement tax calculation") // Detected: TodoInProduction } }
Recommended alternatives:
#![allow(unused)] fn main() { // ✅ GOOD: Propagate errors with ? fn read_config(path: &Path) -> Result<Config> { let content = fs::read_to_string(path)?; parse_config(&content) } // ✅ GOOD: Handle Option explicitly fn get_user(id: u32) -> Result<User> { users.get(&id) .ok_or_else(|| anyhow!("User {} not found", id)) } // ✅ GOOD: Add meaningful context fn parse_value(s: &str) -> Result<i32> { s.parse() .with_context(|| format!("Failed to parse '{}' as integer", s)) } }
Test code exceptions:
#![allow(unused)] fn main() { #[cfg(test)] mod tests { #[test] fn test_parsing() { let result = "42".parse::<i32>().unwrap(); // ✅ OK in tests (LOW priority) assert_eq!(result, 42); } } }
Debtmap detects #[cfg(test)] attributes and test function contexts, automatically assigning Low priority to panic patterns in test code.
Error Propagation Analysis
Debtmap detects missing error context in Result chains:
#![allow(unused)] fn main() { // ❌ Missing context - which file failed? What was the error? fn load_multiple_configs(paths: &[PathBuf]) -> Result<Vec<Config>> { paths.iter() .map(|p| fs::read_to_string(p)) // Error loses file path information .collect::<Result<Vec<_>>>()? .into_iter() .map(|c| parse_config(&c)) // Error loses which config failed .collect() } // ✅ GOOD: Preserve context through the chain fn load_multiple_configs(paths: &[PathBuf]) -> Result<Vec<Config>> { paths.iter() .map(|p| { fs::read_to_string(p) .with_context(|| format!("Failed to read config from {}", p.display())) }) .collect::<Result<Vec<_>>>()? .into_iter() .enumerate() .map(|(i, content)| { parse_config(&content) .with_context(|| format!("Failed to parse config #{}", i)) }) .collect() } }
Best practices:
- Use
.context()or.with_context()fromanyhoworthiserror - Include relevant values in error messages (file paths, indices, input values)
- Maintain error context at each transformation in the chain
Error Swallowing in Rust
#![allow(unused)] fn main() { // ❌ Silent error swallowing fn try_parse(s: &str) -> Option<i32> { match s.parse::<i32>() { Ok(v) => Some(v), Err(_) => None, // Detected: Error swallowed without logging } } // ✅ GOOD: Log the error fn try_parse(s: &str) -> Option<i32> { match s.parse::<i32>() { Ok(v) => Some(v), Err(e) => { log::warn!("Failed to parse '{}': {}", s, e); None } } } }
Python Error Handling Analysis
Bare Except Clause Detection
Python’s bare except: catches all exceptions, including system exits and keyboard interrupts:
# ❌ CRITICAL: Bare except catches everything
def process_file(path):
try:
with open(path) as f:
return f.read()
except: # Detected: BareExceptClause
return None # Catches SystemExit, KeyboardInterrupt, etc.
# ❌ HIGH: Catching Exception is too broad
def load_config(path):
try:
return yaml.load(open(path))
except Exception: # Detected: OverlyBroadException
return {} # Silent failure loses error information
# ✅ GOOD: Specific exception types
def process_file(path):
try:
with open(path) as f:
return f.read()
except FileNotFoundError:
log.error(f"File not found: {path}")
return None
except PermissionError:
log.error(f"Permission denied: {path}")
return None
Why bare except is dangerous:
- Catches
SystemExit(prevents clean shutdown) - Catches
KeyboardInterrupt(prevents Ctrl+C) - Catches
GeneratorExit(breaks generator protocol) - Masks programming errors like
NameError,AttributeError
Best practices:
- Always specify exception types:
except ValueError,except (TypeError, KeyError) - Use
except Exceptiononly when truly catching all application errors - Never use bare
except:in production code - Log exceptions with full context before suppressing
Silent Exception Handling
# ❌ Silent exception handling
def get_user_age(user_id):
try:
user = db.get_user(user_id)
return user.age
except: # Detected: SilentException (no logging, no re-raise)
pass
# ✅ GOOD: Log and provide meaningful default
def get_user_age(user_id):
try:
user = db.get_user(user_id)
return user.age
except UserNotFound:
logger.warning(f"User {user_id} not found")
return None
except DatabaseError as e:
logger.error(f"Database error fetching user {user_id}: {e}")
raise # Re-raise for caller to handle
Exception Flow Analysis
Debtmap analyzes exception propagation through Python codebases:
# Detected: Exception raised but never caught at top level
def process_batch(items):
for item in items:
validate_item(item) # Can raise ValueError
transform_item(item) # Can raise TransformError
save_item(item) # Can raise DatabaseError
# ✅ GOOD: Handle exceptions appropriately
def process_batch(items):
results = {"success": 0, "failed": 0}
for item in items:
try:
validate_item(item)
transform_item(item)
save_item(item)
results["success"] += 1
except ValueError as e:
logger.warning(f"Invalid item {item.id}: {e}")
results["failed"] += 1
except (TransformError, DatabaseError) as e:
logger.error(f"Failed to process item {item.id}: {e}")
results["failed"] += 1
# Optionally re-raise critical errors
if isinstance(e, DatabaseError):
raise
return results
Async Error Handling
Unhandled Promise Rejections (JavaScript/TypeScript)
// ❌ CRITICAL: Unhandled promise rejection
async function loadUserData(userId) {
const response = await fetch(`/api/users/${userId}`);
// If fetch rejects, promise is unhandled
return response.json();
}
loadUserData(123); // Detected: UnhandledPromiseRejection
// ✅ GOOD: Handle rejections
async function loadUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
} catch (error) {
console.error(`Failed to load user ${userId}:`, error);
throw error; // Re-throw or return default
}
}
loadUserData(123).catch(err => {
console.error("Top-level error handler:", err);
});
Missing Await Detection
// ❌ HIGH: Missing await - promise dropped
async function saveAndNotify(data) {
await saveToDatabase(data);
sendNotification(data.userId); // Detected: MissingAwait
// Function returns before notification completes
}
// ✅ GOOD: Await all async operations
async function saveAndNotify(data) {
await saveToDatabase(data);
await sendNotification(data.userId);
}
Async Rust Error Handling
#![allow(unused)] fn main() { // ❌ HIGH: Dropped future without error handling async fn process_requests(requests: Vec<Request>) { for req in requests { tokio::spawn(async move { handle_request(req).await // Detected: DroppedFuture // Errors silently dropped }); } } // ✅ GOOD: Join handles and propagate errors async fn process_requests(requests: Vec<Request>) -> Result<()> { let handles: Vec<_> = requests.into_iter() .map(|req| { tokio::spawn(async move { handle_request(req).await }) }) .collect(); for handle in handles { handle.await??; // Propagate both JoinError and handler errors } Ok(()) } // ❌ HIGH: Task panic silently ignored tokio::spawn(async { panic!("task failed"); // Detected: SilentTaskPanic }); // ✅ GOOD: Handle task panics let handle = tokio::spawn(async { critical_operation().await }); match handle.await { Ok(Ok(result)) => println!("Success: {:?}", result), Ok(Err(e)) => eprintln!("Task failed: {}", e), Err(e) => eprintln!("Task panicked: {}", e), } }
Severity Levels and Prioritization
Error handling issues are assigned severity based on their impact:
| Pattern | Severity | Weight | Priority | Rationale |
|---|---|---|---|---|
| Panic in production | CRITICAL | 4 | Critical | Crashes the process |
| Bare except clause | CRITICAL | 4 | Critical | Masks system signals |
| Silent task panic | CRITICAL | 4 | Critical | Hidden failures |
| Unwrap on Result/Option | HIGH | 4 | High | Likely to panic |
| Dropped future | HIGH | 4 | High | Lost error information |
| Unhandled promise rejection | HIGH | 4 | High | Silently fails |
| Error swallowing | MEDIUM | 4 | Medium | Loses debugging context |
| Missing error context | MEDIUM | 4 | Medium | Hard to debug |
| Expect with generic message | MEDIUM | 4 | Medium | Uninformative errors |
| TODO in production | MEDIUM | 4 | Medium | Incomplete implementation |
All ErrorSwallowing debt has weight 4 (Major severity), but individual patterns receive different priorities based on production impact.
Integration with Risk Scoring
Error handling issues contribute to the debt_factor in Debtmap’s risk scoring formula:
risk_score = (complexity_factor * 0.4) + (debt_factor * 0.3) + (coverage_factor * 0.3)
where debt_factor includes:
- ErrorSwallowing count * weight (4)
- Combined with other debt types
Compound risk example:
#![allow(unused)] fn main() { // HIGH RISK: High complexity + error swallowing + low coverage fn process_transaction(tx: Transaction) -> bool { // Cyclomatic: 12, Cognitive: 18 if tx.amount > 1000 { if tx.verified { if validate_funds(&tx).unwrap() { // ❌ Panic pattern if tx.user_type == "premium" { match apply_premium_discount(&tx) { Ok(_) => {}, Err(_) => return false, // ❌ Error swallowed } } charge_account(&tx).unwrap(); // ❌ Another panic return true; } } } false } // Coverage: 45% (untested error paths) // Risk Score: Very High (complexity + error handling + coverage gaps) }
This function would be flagged as Priority 1 in Debtmap’s output due to:
- High cyclomatic complexity (12)
- Multiple panic patterns (unwrap calls)
- Error swallowing (ignored Result)
- Coverage gaps in error handling paths
Configuration
Error Handling Configuration Options
Configure error handling analysis in .debtmap.toml:
[error_handling]
# Patterns to detect (all enabled by default)
patterns = [
"panic_patterns", # Rust unwrap/expect/panic
"error_swallowing", # Silent exception handling
"bare_except", # Python bare except clauses
"async_errors", # Unhandled promises, dropped futures
"missing_context", # Error propagation without context
]
# Severity levels for different error types
severity_levels = { panic = "critical", bare_except = "critical", error_swallowing = "major" }
# Error context requirements
context_requirements = { min_context_length = 10, require_values = true }
# Exclude specific patterns (useful for legacy code migration)
exclude_patterns = [
# "unwrap_in_tests", # Already excluded by default
]
Custom Severity Overrides
[debt_categories.ErrorSwallowing]
weight = 4 # Default is 4 (Major severity)
severity = "Major"
description = "Silenced exceptions"
# Lower weight for gradual adoption
# weight = 2
# severity = "Warning"
Detection Examples
What Gets Detected vs. Not Detected
Rust examples:
#![allow(unused)] fn main() { // ❌ Detected: unwrap() in production code pub fn get_config() -> Config { load_config().unwrap() } // ✅ Not detected: ? operator (proper error propagation) pub fn get_config() -> Result<Config> { load_config()? } // ✅ Not detected: unwrap() in test #[test] fn test_config() { let config = load_config().unwrap(); // OK in tests assert_eq!(config.port, 8080); } // ❌ Detected: expect() with generic message let value = map.get("key").expect("missing"); // ✅ Not detected: expect() with descriptive context let value = map.get("key") .expect("Configuration must contain 'key' field"); }
Python examples:
# ❌ Detected: bare except
try:
risky_operation()
except:
pass
# ✅ Not detected: specific exception
try:
risky_operation()
except ValueError:
handle_value_error()
# ❌ Detected: silent exception (no logging/re-raise)
try:
db.save(record)
except DatabaseError:
pass # Silent failure
# ✅ Not detected: logged exception
try:
db.save(record)
except DatabaseError as e:
logger.error(f"Failed to save record: {e}")
raise
Suppression Patterns
For cases where error handling patterns are intentional, use suppression comments:
Rust:
#![allow(unused)] fn main() { // debtmap: ignore - Unwrap is safe here due to prior validation let value = validated_map.get("key").unwrap(); }
Python:
try:
experimental_feature()
except: # debtmap: ignore - Intentional catch-all during migration
use_fallback()
See Suppression Patterns for complete syntax and usage.
Best Practices
Rust Error Handling
-
Prefer
?operator over unwrap/expect#![allow(unused)] fn main() { // Instead of: fs::read_to_string(path).unwrap() // Use: fs::read_to_string(path)? } -
Use anyhow for application errors, thiserror for libraries
#![allow(unused)] fn main() { use anyhow::{Context, Result}; fn load_data(path: &Path) -> Result<Data> { let content = fs::read_to_string(path) .with_context(|| format!("Failed to read {}", path.display()))?; parse_data(&content) .context("Invalid data format") } } -
Add context at each error boundary
#![allow(unused)] fn main() { .with_context(|| format!("meaningful message with {}", value)) } -
Handle Option explicitly
#![allow(unused)] fn main() { map.get(key).ok_or_else(|| anyhow!("Missing key: {}", key))? }
Python Error Handling
-
Always use specific exception types
except (ValueError, KeyError) as e: -
Log before suppressing
except DatabaseError as e: logger.error(f"Database operation failed: {e}", exc_info=True) # Then decide: re-raise, return default, or handle -
Avoid bare except completely
# If you must catch everything: except Exception as e: # Not bare except: logger.exception("Unexpected error") raise -
Use context managers for resource cleanup
with open(path) as f: # Ensures cleanup even on exception process(f)
JavaScript/TypeScript Error Handling
-
Always handle promise rejections
fetchData().catch(err => console.error(err)); // Or use try/catch with async/await -
Use async/await consistently
async function process() { try { const data = await fetchData(); await saveData(data); } catch (error) { console.error("Failed:", error); throw error; } } -
Don’t forget await
await asyncOperation(); // Don't drop promises
Improving Error Handling Based on Debtmap Reports
Workflow
-
Run analysis with error focus
debtmap analyze --debt-type ErrorSwallowing -
Review priority issues first
- Address CRITICAL (panic in production, bare except) immediately
- Schedule HIGH (unwrap, dropped futures) for next sprint
- Plan MEDIUM (missing context) for gradual improvement
-
Fix systematically
- One file or module at a time
- Add tests as you improve error handling
- Run debtmap after each fix to verify
-
Validate improvements
# Before fixes debtmap analyze --output before.json # After fixes debtmap analyze --output after.json # Compare debtmap compare before.json after.json
Migration Strategy for Legacy Code
# .debtmap.toml - Gradual adoption
[error_handling]
# Start with just critical issues
patterns = ["panic_patterns", "bare_except"]
# After fixing critical issues, add more patterns
# patterns = ["panic_patterns", "bare_except", "error_swallowing"]
# Eventually enable all patterns
# patterns = ["panic_patterns", "error_swallowing", "bare_except", "async_errors", "missing_context"]
Track progress over time:
# Weekly error handling health check
debtmap analyze --debt-type ErrorSwallowing | tee weekly-error-health.txt
Troubleshooting
Too Many False Positives in Test Code
Problem: Debtmap flagging unwrap() in test functions
Solution: Debtmap should automatically detect test code via:
#[cfg(test)]modules in Rust#[test]attributestest_function name prefix in Python*.test.ts,*.spec.jsfile patterns
If false positives persist:
#![allow(unused)] fn main() { // Use suppression comment let value = result.unwrap(); // debtmap: ignore - Test assertion }
Error Patterns Not Being Detected
Problem: Known error patterns not appearing in report
Causes and solutions:
-
Language support not enabled
debtmap analyze --languages rust,python,javascript -
Pattern disabled in config
[error_handling] patterns = ["panic_patterns", "error_swallowing"] # Ensure pattern is listed -
Suppression comment present
- Check for
debtmap: ignorecomments - Review
.debtmap.tomlignore patterns
- Check for
Disagreement with Severity Levels
Problem: Severity feels too high/low for your codebase
Solution: Customize in .debtmap.toml:
[debt_categories.ErrorSwallowing]
weight = 2 # Reduce from default 4 to Warning level
severity = "Warning"
# Or increase for stricter enforcement
# weight = 5
# severity = "Critical"
Can’t Find Which Line Has the Issue
Problem: Debtmap reports error at wrong line number
Causes:
- Source code changed since analysis
- Parser approximation for line numbers
Solutions:
- Re-run analysis:
debtmap analyze - Search for pattern:
rg "\.unwrap\(\)" src/ - Enable debug logging:
debtmap analyze --log-level debug
Validating Error Handling Improvements
Problem: Unsure if fixes actually improved code quality
Solution: Use compare workflow:
# Baseline before fixes
git checkout main
debtmap analyze --output baseline.json
# After fixes
git checkout feature/improve-errors
debtmap analyze --output improved.json
# Compare reports
debtmap compare baseline.json improved.json
Look for:
- Reduced ErrorSwallowing debt count
- Lower risk scores for affected functions
- Improved coverage of error paths (if running with coverage)
Related Topics
- Configuration - Complete
.debtmap.tomlreference - Suppression Patterns - Suppress false positives
- Scoring Strategies - How error handling affects risk scores
- Coverage Integration - Detect untested error paths
- CLI Reference - Command-line options for error analysis
- Troubleshooting - General debugging guide