Domain Vs Transport
Learn about the distinction between domain errors and transport errors in Goa, and how to effectively map between them.
Goa provides a robust error handling system that enables you to define, manage, and communicate errors effectively across your services. This guide covers everything you need to know about error handling in Goa.
Goa takes a “battery included” approach to error handling where errors can be defined with minimal information (just a name) while also supporting completely custom error types when needed. The framework generates both code and documentation from your error definitions, ensuring consistency across your API.
Key features of Goa’s error handling:
Errors can be defined at the API level to create reusable error definitions. Unlike service-level errors, API-level errors don’t automatically apply to all methods. Instead, they provide a way to define error properties, including transport mappings, once and reuse them across services and methods:
var _ = API("calc", func() {
// Define reusable error with transport mapping
Error("invalid_argument") // Uses default ErrorResult type
HTTP(func() {
Response("invalid_argument", StatusBadRequest)
})
})
var _ = Service("divider", func() {
// Reference the API-level error
Error("invalid_argument") // Reuses error defined above
// No need to define HTTP mapping again
Method("divide", func() {
Payload(DivideRequest)
// Method-specific error with custom type
Error("div_by_zero", DivByZero, "Division by zero")
})
})
This approach:
Service-level errors are available to all methods within a service. Unlike API-level errors which provide reusable definitions, service-level errors automatically apply to every method in the service:
var _ = Service("calc", func() {
// This error can be returned by any method in this service
Error("invalid_arguments", ErrorResult, "Invalid arguments provided")
Method("divide", func() {
// This method can return invalid_arguments without explicitly declaring it
Payload(func() {
Field(1, "dividend", Int)
Field(2, "divisor", Int)
Required("dividend", "divisor")
})
// ... other method definitions
})
Method("multiply", func() {
// This method can also return invalid_arguments
// ... method definitions
})
})
When defining errors at the service level:
Method-specific errors are scoped to a particular method:
var _ = Service("calc", func() {
Method("divide", func() {
Payload(func() {
Field(1, "dividend", Int)
Field(2, "divisor", Int)
Required("dividend", "divisor")
})
Result(func() {
Field(1, "quotient", Int)
Field(2, "reminder", Int)
Required("quotient", "reminder")
})
Error("div_by_zero") // Method-specific error
})
})
For more complex error scenarios, you can define custom error types. Custom error types allow you to include additional contextual information specific to your error cases.
Here’s a simple custom error type:
var DivByZero = Type("DivByZero", func() {
Description("DivByZero is the error returned when using value 0 as divisor.")
Field(1, "message", String, "division by zero leads to infinity.")
Required("message")
})
When using custom error types for multiple errors in the same method, Goa needs to know which field contains the error name. This is crucial for:
To specify the error name field, use the struct:error:name
metadata:
var DivByZero = Type("DivByZero", func() {
Description("DivByZero is the error returned when using value 0 as divisor.")
Field(1, "message", String, "division by zero leads to infinity.")
Field(2, "name", String, "Name of the error", func() {
Meta("struct:error:name") // Tells Goa this field contains the error name
})
Required("message", "name")
})
The field marked with Meta("struct:error:name")
:
"error_name"
(reserved by Goa)When a method can return multiple different custom error types, the name field becomes especially important. Here’s why:
Error Type Resolution: When multiple error types are possible, Goa uses the name field to determine which error definition in the design matches the actual error being returned. This allows Goa to:
Transport Layer Handling: Without the name field, the transport layer wouldn’t know which status code to use when multiple error types are defined with different status codes:
HTTP(func() {
Response("div_by_zero", StatusBadRequest) // 400
Response("overflow", StatusUnprocessableEntity) // 422
})
Client-Side Type Assertion: The name field enables Goa to generate specific error types for each error defined in your design. These generated types make error handling type-safe and provide access to all error fields:
Here’s an example showing how error names in the design must match the implementation:
var _ = Service("calc", func() {
Method("divide", func() {
// These names ("div_by_zero" and "overflow") must be used exactly
// in the error type's name field
Error("div_by_zero", DivByZero)
Error("overflow", NumericOverflow)
// ... other method definitions
})
})
// Example client code handling these errors
res, err := client.Divide(ctx, payload)
if err != nil {
switch err := err.(type) {
case *calc.DivideDivByZeroError:
// This error corresponds to Error("div_by_zero", ...) in the design
fmt.Printf("Division by zero error: %s\n", err.Message)
fmt.Printf("Attempted to divide %d by zero\n", err.Dividend)
case *calc.DivideOverflowError:
// This error corresponds to Error("overflow", ...) in the design
fmt.Printf("Overflow error: %s\n", err.Message)
fmt.Printf("Result value %d exceeded maximum\n", err.Value)
case *goa.ServiceError:
// Handle general service errors (validation, etc)
fmt.Printf("Service error: %s\n", err.Message)
default:
// Handle unknown errors
fmt.Printf("Unknown error: %s\n", err.Error())
}
}
For each error defined in your design, Goa generates:
DivideDivByZeroError
for "div_by_zero"
)The connection between design and implementation is maintained through the error names:
Error("name", ...)
in the designMethodNameError
)Error properties are crucial flags that inform clients about the nature of
errors and enable them to implement appropriate handling strategies. These
properties are only available when using the default ErrorResult
type -
they have no effect when using custom error types.
The properties are defined using DSL functions:
Temporary()
: Indicates the error is transient and the same request may succeed if retriedTimeout()
: Indicates the error occurred because a deadline was exceededFault()
: Indicates a server-side error (bug, configuration issue, etc.)When using the default ErrorResult
type, these properties are automatically
mapped to fields in the generated ServiceError
struct, enabling sophisticated
client-side error handling:
var _ = Service("calc", func() {
// Temporary errors suggest the client should retry
Error("service_unavailable", ErrorResult, func() { // Note: using ErrorResult type
Description("Service is temporarily unavailable")
Temporary() // Sets the Temporary field in ServiceError
})
// Timeout errors help clients adjust their timeouts
Error("request_timeout", ErrorResult, func() { // Note: using ErrorResult type
Description("Request timed out")
Timeout() // Sets the Timeout field in ServiceError
})
// Fault errors indicate server issues that require administrator attention
Error("internal_error", ErrorResult, func() { // Note: using ErrorResult type
Description("Internal server error")
Fault() // Sets the Fault field in ServiceError
})
})
Clients can then use these properties to implement sophisticated error handling:
res, err := client.Divide(ctx, payload)
if err != nil {
switch e := err.(type) {
case *goa.ServiceError: // Only ServiceError has these properties
if e.Temporary {
// Implement retry with backoff
return retry(ctx, func() error {
res, err = client.Divide(ctx, payload)
return err
})
}
if e.Timeout {
// Maybe increase timeout for next request
ctx = context.WithTimeout(ctx, 2*time.Second)
return client.Divide(ctx, payload)
}
if e.Fault {
// Log error and alert administrators
log.Error("server fault detected", "error", e)
alertAdmins(e)
}
default:
// Custom error types won't have these properties
log.Error("error occurred", "error", err)
}
}
These properties enable clients to:
Note: If you need these properties with custom error types, you’ll need to implement similar fields in your custom type and handle them explicitly in your code.
Goa allows you to map errors to appropriate transport-specific status codes. This mapping is crucial for providing consistent and meaningful error responses across different protocols.
For HTTP transport, map errors to standard HTTP status codes that best represent
the error condition. The mapping is defined in the HTTP
DSL:
var _ = Service("calc", func() {
Method("divide", func() {
// Define possible errors with their descriptions
Error("div_by_zero", ErrorResult, "Division by zero error")
Error("overflow", ErrorResult, "Numeric overflow error")
Error("unauthorized", ErrorResult, "Authentication required")
HTTP(func() {
POST("/")
// Map each error to an appropriate HTTP status code
Response("div_by_zero", StatusBadRequest)
Response("overflow", StatusUnprocessableEntity)
Response("unauthorized", StatusUnauthorized)
})
})
})
When an error occurs, Goa will:
For gRPC transport, map errors to standard gRPC status codes. The mapping follows similar principles but uses gRPC-specific codes:
var _ = Service("calc", func() {
Method("divide", func() {
// Define possible errors with their descriptions
Error("div_by_zero", ErrorResult, "Division by zero error")
Error("overflow", ErrorResult, "Numeric overflow error")
Error("unauthorized", ErrorResult, "Authentication required")
GRPC(func() {
// Map each error to an appropriate gRPC status code
Response("div_by_zero", CodeInvalidArgument)
Response("overflow", CodeOutOfRange)
Response("unauthorized", CodeUnauthenticated)
})
})
})
Common gRPC status code mappings:
CodeInvalidArgument
: For validation errors (e.g., div_by_zero)CodeNotFound
: For resource not found errorsCodeUnauthenticated
: For authentication errorsCodePermissionDenied
: For authorization errorsCodeDeadlineExceeded
: For timeout errorsCodeInternal
: For server-side faultsIf no explicit mapping is provided:
CodeUnknown
for unmapped errorsNow that you understand the basics of error handling in Goa, explore these topics to deepen your knowledge:
These guides will help you implement a comprehensive error handling strategy in your Goa services.