Error Handling Guidelines
February 5, 2026 ยท View on GitHub
This document describes error handling conventions for the fxios codebase.
Error Type Conventions
All custom errors should:
- Be defined as
enumtypes - Conform to
ErrorandCustomStringConvertible - Include context-rich associated values
- Preserve underlying errors when wrapping
Example: Gold Standard Pattern
enum MyError: Error, CustomStringConvertible {
/// Operation failed due to an underlying error.
case operationFailed(reason: String, underlyingError: Error?)
/// Validation failed with specific details.
case validationFailed(details: String)
var description: String {
switch self {
case .operationFailed(let reason, let underlyingError):
var message = "Operation failed: \(reason)"
if let error = underlyingError {
message += " (\(error.localizedDescription))"
}
return message
case .validationFailed(let details):
return "Validation failed: \(details)"
}
}
}
When to Use underlyingError
Include underlyingError: Error? when:
- Catching a thrown error and re-throwing a domain-specific error
- Wrapping system errors (file I/O, network, JSON parsing)
- Converting ShellRunnerError to command-specific errors
Do NOT include when:
- The error is a validation failure with no underlying cause
- The error represents a missing resource (not found)
Throwing vs Herald Reporting
Throw when:
- The error should stop command execution
- The error is recoverable at a higher level
- You're in a utility/helper function
Use Herald.declare(asError: true) when:
- Warning the user but continuing execution
- Reporting non-fatal issues mid-command
- The error has already been handled
Never:
- Silently catch and ignore errors
- Use empty catch blocks without at least logging
// BAD: Silent failure
do {
try something()
} catch {
// Silent - user has no idea what happened
}
// GOOD: Report the error
do {
try something()
} catch {
Herald.declare("Could not do something: \(error)", asError: true)
}
Debug Logging
Use Logger for debug output that helps troubleshooting:
Logger.debug("Starting operation with \(args)")
Logger.error("Operation failed", error: error)
Debug output:
- Only appears when
--debugflag is passed - Goes to stderr (doesn't interfere with normal output)
- Includes file/line for tracing
Log Levels
Logger.debug()- Detailed trace information for debuggingLogger.info()- General informational messagesLogger.warning()- Potential issues that don't stop executionLogger.error()- Errors with optional underlying error details
When to Log
Add debug logging at:
- Entry points of significant operations
- Before and after external process execution (ShellRunner)
- When errors occur (always include the underlying error)
- At decision points that affect behavior
Testing Errors
Every error case should have tests verifying:
- Error message contains expected context
- Underlying errors are preserved
- Error conforms to required protocols
See L10nErrorTests.swift for the pattern:
@Test("operationFailed includes reason and error details")
func operationFailedMessage() {
let underlyingError = TestError(message: "Connection refused")
let error = MyError.operationFailed(
reason: "Failed to connect",
underlyingError: underlyingError
)
let description = error.description
#expect(description.contains("Failed to connect"))
#expect(description.contains("Connection refused"))
}
Error Handling Checklist
When adding new error handling code:
- Custom error enum conforms to
ErrorandCustomStringConvertible - Cases that wrap errors include
underlyingError: Error? - Description includes underlying error when present
- No empty catch blocks - always log or report
- Debug logging added at key decision points
- Tests cover all error cases
Examples from the Codebase
L10nError (Gold Standard)
case fileReadFailed(path: String, underlyingError: Error)
var description: String {
case .fileReadFailed(let path, let error):
return "Failed to read file at '\(path)': \(error.localizedDescription)"
}
SimulatorManagerError
case simctlFailed(reason: String, underlyingError: Error?)
var description: String {
case .simctlFailed(let reason, let underlyingError):
var message = "simctl command failed: \(reason)"
if let error = underlyingError {
message += " (\(error.localizedDescription))"
}
return message
}
RepoDetectorError
case invalidMarkerFile(reason: String, underlyingError: Error?)