# Error Handling Quick Reference Guide **For Cain Developers** This guide provides quick examples of proper error handling patterns used in Cain. --- ## Python Error Handling Patterns ### 1. Subprocess Execution with Retry ```python import subprocess import time MAX_RETRIES = 3 BASE_TIMEOUT = 30 # seconds def run_with_retry(command, timeout=BASE_TIMEOUT): """Run subprocess with retry and exponential backoff.""" for attempt in range(MAX_RETRIES): try: result = subprocess.run( command, timeout=timeout, check=True, capture_output=True, text=True ) return result except subprocess.TimeoutExpired: if attempt < MAX_RETRIES - 1: backoff = min(2 ** attempt, 30) # Max 30s print(f"Timeout, retrying in {backoff}s...") time.sleep(backoff) else: raise except subprocess.CalledProcessError as e: print(f"Command failed: {e.stderr}") raise ``` ### 2. Network API Call with Timeout ```python import requests from requests.adapters import HTTPAdapter from requests.packages.urllib3.util.retry import Retry def create_resilient_session(): """Create a requests session with automatic retry.""" session = requests.Session() retry = Retry( total=3, backoff_factor=1, status_forcelist=[429, 500, 502, 503, 504], ) adapter = HTTPAdapter(max_retries=retry) session.mount('http://', adapter) session.mount('https://', adapter) return session def safe_api_call(url, timeout=30): """Make API call with proper error handling.""" try: response = create_resilient_session().get(url, timeout=timeout) response.raise_for_status() return response.json() except requests.Timeout: print(f"API timeout: {url}") return None except requests.ConnectionError: print(f"Connection failed: {url}") return None except requests.HTTPError as e: print(f"HTTP error {e.response.status_code}: {url}") return None ``` ### 3. File Operations with Atomic Write ```python import os import json def save_atomically(filepath, data): """Save data atomically to prevent corruption.""" temp_path = filepath + ".tmp" try: # Write to temp file first with open(temp_path, 'w') as f: json.dump(data, f) # Atomic rename os.rename(temp_path, filepath) return True except (OSError, IOError) as e: print(f"Failed to save {filepath}: {e}") # Clean up temp file if os.path.exists(temp_path): os.unlink(temp_path) return False def load_with_fallback(filepath, default=None): """Load file with corrupt data recovery.""" try: with open(filepath, 'r') as f: return json.load(f) except (json.JSONDecodeError, ValueError) as e: print(f"Corrupt file {filepath}: {e}") # Backup corrupt file backup_path = f"{filepath}.corrupt.{int(time.time())}" os.rename(filepath, backup_path) print(f"Backed up to {backup_path}") return default if default is not None else {} except (OSError, IOError) as e: print(f"Cannot read {filepath}: {e}") return default if default is not None else {} ``` ### 4. Context Manager for Resources ```python from contextlib import contextmanager @contextmanager def safe_resource(resource): """Context manager that guarantees cleanup.""" try: yield resource except Exception as e: print(f"Error with {resource}: {e}") raise finally: # Always cleanup, even on error resource.cleanup() ``` --- ## JavaScript/TypeScript Error Handling Patterns ### 1. Async Function with Timeout ```javascript async function withTimeout(promise, timeoutMs, errorMessage = "Operation timed out") { const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error(errorMessage)), timeoutMs) ); try { return await Promise.race([promise, timeout]); } catch (error) { console.error(`${errorMessage}:`, error); throw error; } } // Usage const result = await withTimeout( fetch('/api/data'), 10000, 'API request timeout' ); ``` ### 2. Fetch with Retry and Backoff ```javascript async function fetchWithRetry(url, options = {}, maxRetries = 3) { for (let attempt = 0; attempt < maxRetries; attempt++) { try { const response = await fetch(url, { ...options, signal: AbortSignal.timeout(30000) // 30s timeout }); if (response.ok) return response; if (response.status >= 500 && attempt < maxRetries - 1) { // Retry server errors with backoff const backoff = Math.min(1000 * Math.pow(2, attempt), 10000); console.log(`Retry ${attempt + 1}/${maxRetries} after ${backoff}ms`); await new Promise(r => setTimeout(r, backoff)); continue; } return response; } catch (error) { if (attempt === maxRetries - 1) throw error; const backoff = Math.min(1000 * Math.pow(2, attempt), 10000); console.log(`Network error, retry ${attempt + 1}/${maxRetries} after ${backoff}ms`); await new Promise(r => setTimeout(r, backoff)); } } } ``` ### 3. localStorage with Fallback ```javascript class SafeStorage { constructor() { this.memory = new Map(); this.available = this._checkAvailable(); } _checkAvailable() { try { const test = '__storage_test__'; localStorage.setItem(test, test); localStorage.removeItem(test); return true; } catch { return false; } } setItem(key, value) { if (this.available) { try { localStorage.setItem(key, value); return true; } catch (e) { console.warn('localStorage failed, using memory:', e); } } this.memory.set(key, value); return false; } getItem(key) { if (this.available) { const value = localStorage.getItem(key); if (value !== null) return value; } return this.memory.get(key) || null; } } const storage = new SafeStorage(); ``` ### 4. Error Boundary (React-like pattern) ```javascript function wrapWithErrorBoundary(fn) { return function(...args) { try { return fn(...args); } catch (error) { console.error('Error in', fn.name, ':', error); // Show user-friendly error showErrorToUser('An unexpected error occurred. Please refresh.'); // Optionally send to error tracking reportError(error, fn.name, args); return null; // or fallback value }; }; } // Or for async functions async function wrapAsyncWithErrorBoundary(fn) { return async function(...args) { try { return await fn(...args); } catch (error) { console.error('Async error in', fn.name, ':', error); showErrorToUser('Operation failed. Please try again.'); reportError(error, fn.name, args); return null; } }; } ``` --- ## Git Operations Error Handling ### Pattern from Cain (git_repo.py) ```python import subprocess import time from typing import List, Optional class GitTimeoutError(Exception): """Git operation timed out.""" pass class GitOperationError(Exception): """Git operation failed.""" pass def run_git_command( args: List[str], timeout: int = 45, max_retries: int = 3, cwd: str = "." ) -> subprocess.CompletedProcess: """Run git command with timeout and retry.""" attempt = 0 while attempt <= max_retries: try: start = time.time() result = subprocess.run( ["git"] + args, cwd=cwd, timeout=timeout, check=True, capture_output=True, text=True ) elapsed = time.time() - start print(f"Git completed in {elapsed:.2f}s: {args[0]}") return result except subprocess.TimeoutExpired: attempt += 1 if attempt <= max_retries: backoff = min(2 ** (attempt - 1), 30) print(f"Git timeout, retry {attempt}/{max_retries} in {backoff}s") time.sleep(backoff) else: raise GitTimeoutError(f"Git timed out after {max_retries} retries") except subprocess.CalledProcessError as e: # Don't retry on fatal errors if "fatal:" in (e.stderr or ""): raise GitOperationError(f"Git fatal error: {e.stderr}") attempt += 1 if attempt <= max_retries: backoff = min(2 ** (attempt - 1), 30) print(f"Git failed, retry {attempt}/{max_retries} in {backoff}s") time.sleep(backoff) else: raise GitOperationError(f"Git failed: {e.stderr}") ``` --- ## Toast Notification System ```javascript // Add to electron-standalone.html const toastContainer = document.createElement('div'); toastContainer.id = 'toast-container'; toastContainer.style.cssText = ` position: fixed; top: 20px; right: 20px; z-index: 10000; `; document.body.appendChild(toastContainer); function showToast(message, type = 'info', duration = 5000) { const toast = document.createElement('div'); toast.className = `toast toast-${type}`; toast.textContent = message; toast.style.cssText = ` padding: 12px 20px; margin-bottom: 10px; border-radius: 8px; background: ${type === 'error' ? '#e94560' : type === 'success' ? '#4caf50' : '#333'}; color: white; box-shadow: 0 4px 12px rgba(0,0,0,0.3); animation: slideIn 0.3s ease; `; toastContainer.appendChild(toast); setTimeout(() => { toast.style.animation = 'slideOut 0.3s ease'; setTimeout(() => toast.remove(), 300); }, duration); } // Usage examples try { localStorage.setItem('setting', value); } catch (e) { showToast('Could not save settings', 'error'); } // Network status window.addEventListener('offline', () => showToast('Connection lost', 'error')); window.addEventListener('online', () => showToast('Back online', 'success')); ``` --- ## Best Practices Summary 1. **Always handle timeouts** - Network operations should have explicit timeouts 2. **Use atomic writes** - Write to temp file, then rename 3. **Log before suppressing** - Never silently catch without logging 4. **Provide user feedback** - Show toasts for user-facing errors 5. **Retry with backoff** - Exponential backoff prevents thundering herd 6. **Degrade gracefully** - Local should work when remote fails 7. **Clean up resources** - Use try/finally or context managers 8. **Validate early** - Check prerequisites before attempting operations 9. **Use specific exceptions** - Custom exception types for different errors 10. **Document error codes** - For support and debugging --- ## Common Pitfalls to Avoid ❌ **DON'T:** ```python try: risky_operation() except: pass # Silent failure - hard to debug! ``` ✅ **DO:** ```python try: risky_operation() except SpecificError as e: logger.warning(f"Operation failed: {e}") # Handle appropriately ``` ❌ **DON'T:** ```python subprocess.run(command, check=True) # Crashes on failure ``` ✅ **DO:** ```python try: result = subprocess.run(command, check=True, timeout=30) except subprocess.TimeoutExpired: logger.error("Command timed out") except subprocess.CalledProcessError as e: logger.error(f"Command failed: {e}") ``` --- **Remember:** Good error handling makes your application: - More reliable (fewer crashes) - Easier to debug (better logging) - Better for users (clear feedback) - Easier to maintain (predictable behavior)