Error Design Principles
View SourceThis document explains the design philosophy and principles behind the ErrorMessage library, helping you understand why it was created and how it can improve your application's error handling.
Why Consistent Error Handling Matters
Error handling is a critical aspect of any robust application. Inconsistent error handling can lead to:
- Unpredictable behavior: When errors are represented differently across your application, it becomes difficult to predict how errors will be structured.
- Fragile error handling: String-based error matching is brittle and can break when error messages change.
- Poor user experience: Inconsistent error reporting leads to confusing user interfaces and API responses.
- Difficult debugging: Without structured errors, it's harder to track down the root cause of issues.
ErrorMessage addresses these problems by providing a consistent structure for all errors in your application.
HTTP Status Codes as a Universal Language
ErrorMessage uses HTTP status codes as the foundation for error classification. This approach has several advantages:
- Universality: HTTP status codes are widely understood across programming languages and platforms.
- Semantic meaning: Each status code has a well-defined meaning (e.g., 404 for "not found", 403 for "forbidden").
- Standardization: Using HTTP status codes means your error system aligns with web standards.
- API compatibility: When building web APIs, your internal error representation maps directly to HTTP responses.
Even for non-web applications, HTTP status codes provide a comprehensive and well-understood taxonomy of error conditions that can be applied to many domains.
The Three-Part Error Structure
ErrorMessage uses a three-part structure for errors:
- Code: An atom representing the error type (e.g.,
:not_found
,:internal_server_error
) - Message: A human-readable description of what went wrong
- Details: Additional context-specific information about the error
This structure provides a balance between:
- Simplicity: The core structure is easy to understand and use
- Flexibility: The details field can contain any data structure needed for your specific use case
- Consistency: All errors follow the same pattern, making them predictable
Pattern Matching vs. String Matching
One of the key benefits of ErrorMessage is the ability to pattern match on error codes rather than error messages:
# Fragile approach (depends on exact message text)
case result do
{:error, "User not found: " <> _} -> handle_not_found()
# ...
end
# Robust approach with ErrorMessage
case result do
{:error, %ErrorMessage{code: :not_found}} -> handle_not_found()
# ...
end
Pattern matching on error codes is:
- More robust: Changing error messages doesn't break your error handling logic
- More explicit: The intent of your code is clearer
- More maintainable: Error handling code is less likely to break over time
Serialization Considerations
ErrorMessage includes built-in support for converting errors to strings (for logging) and maps (for JSON serialization). This addresses common challenges with error serialization:
- Complex data structures: The
ensure_json_serializable/1
function handles conversion of Elixir-specific data types (like PIDs, Date/Time structs, etc.) to JSON-compatible formats. - Request context: When available, the request ID is automatically included in serialized errors, making it easier to correlate errors with specific requests.
- Consistent output: All errors are serialized in a consistent format, making it easier to process them in clients.
Integration with Existing Systems
ErrorMessage is designed to integrate seamlessly with:
- Phoenix: For web API error responses
- Logger: For structured error logging
- Plug: For HTTP status code mapping
This integration-friendly design means you can adopt ErrorMessage incrementally in your application without having to rewrite existing code all at once.
When to Use ErrorMessage
ErrorMessage is particularly valuable in:
- API-driven applications: Where consistent error responses are crucial
- Microservice architectures: Where errors might cross service boundaries
- Large codebases: Where consistency across modules is important
- Long-lived applications: Where maintainability over time is a priority
While ErrorMessage can be used in any Elixir application, these scenarios highlight where it provides the most value.