When designing error handling in Goa, it’s important to understand the distinction between domain errors and their transport representation. This separation allows you to maintain clean domain logic while ensuring proper error communication across different protocols.
Domain errors represent business logic failures in your application. They are
protocol-agnostic and focus on what went wrong from a business logic
perspective. Goa’s default ErrorResult
type is often sufficient for expressing
domain errors - custom error types are optional and only needed for specialized
cases.
The default ErrorResult
type combined with meaningful names, descriptions, and
error properties can effectively express most domain errors:
var _ = Service("payment", func() {
// Define domain errors using default ErrorResult type
Error("insufficient_funds", ErrorResult, func() {
Description("Account has insufficient funds for the transaction")
// Error properties help define error characteristics
Temporary() // Error may resolve if user adds funds
})
Error("card_expired", ErrorResult, func() {
Description("Payment card has expired")
// This is a permanent error until card is updated
})
Error("processing_failed", ErrorResult, func() {
Description("Payment processing system temporarily unavailable")
Temporary() // Can retry later
Fault() // Server-side issue
})
Method("process", func() {
// ... method definition
})
})
Domain errors should:
For cases where additional structured error data is needed, you can define custom error types. See the
main error handling documentation for detailed information about
custom error types, including important requirements for the name
field and the struct:error:name
metadata.
// Custom type for when extra error context is required
var PaymentError = Type("PaymentError", func() {
Description("PaymentError represents a failure in payment processing")
Field(1, "message", String, "Human-readable error message")
Field(2, "code", String, "Internal error code")
Field(3, "transaction_id", String, "Failed transaction ID")
Field(4, "name", String, "Error name for transport mapping", func() {
Meta("struct:error:name")
})
Required("message", "code", "name")
})
Transport mappings define how domain errors are represented in specific protocols. This includes status codes, headers, and response formats.
var _ = Service("payment", func() {
// Define domain errors
Error("insufficient_funds", PaymentError)
Error("card_expired", PaymentError)
Error("processing_failed", PaymentError)
HTTP(func() {
// Map domain errors to HTTP status codes
Response("insufficient_funds", StatusPaymentRequired, func() {
// Add payment-specific headers
Header("Retry-After")
// Customize error response format
Body(func() {
Attribute("error_code")
Attribute("message")
})
})
Response("card_expired", StatusUnprocessableEntity)
Response("processing_failed", StatusServiceUnavailable)
})
})
var _ = Service("payment", func() {
// Same domain errors
Error("insufficient_funds", PaymentError)
Error("card_expired", PaymentError)
Error("processing_failed", PaymentError)
GRPC(func() {
// Map to gRPC status codes
Response("insufficient_funds", CodeFailedPrecondition)
Response("card_expired", CodeInvalidArgument)
Response("processing_failed", CodeUnavailable)
})
})
This separation of concerns provides several advantages:
Protocol Independence
Consistent Error Handling
Better Documentation
Here’s how this separation works in practice:
func (s *paymentService) Process(ctx context.Context, p *payment.ProcessPayload) (*payment.ProcessResult, error) {
// Domain logic
if !hasEnoughFunds(p.Amount) {
// Return error using generated helper function
return nil, payment.MakeInsufficientFunds(
fmt.Errorf("account balance %d below required amount %d", balance, p.Amount))
}
if isSystemOverloaded() {
// Return error for temporary system issue
return nil, payment.MakeProcessingFailed(
fmt.Errorf("payment system temporarily unavailable"))
}
// More processing...
}
func (s *paymentService) Process(ctx context.Context, p *payment.ProcessPayload) (*payment.ProcessResult, error) {
// Domain logic
if !hasEnoughFunds(p.Amount) {
// Return domain error with additional context
return nil, &payment.PaymentError{
Name: "insufficient_funds",
Message: "Account balance too low for transaction",
Code: "FUNDS_001",
TransactionID: txID,
}
}
// More processing...
}
The transport layer automatically:
Domain First
Consistent Mapping
Error Properties
Temporary()
, Timeout()
, Fault()
) to indicate error characteristicsDocumentation
By separating domain errors from their transport representation, Goa enables you to: