Goa provides a flexible and powerful way to define errors within your service designs. By leveraging Goa’s Domain-Specific Language (DSL), you can specify both service-level and method-level errors, customize error types, and ensure that your API communicates failures clearly and consistently across different transports like HTTP and gRPC.
Service-level errors are defined at the service scope and can be returned by any method within the service. This is useful for errors that are common across multiple methods.
var _ = Service("divider", func() {
// The "DivByZero" error is defined at the service level and
// thus may be returned by both "divide" and "integral_divide".
Error("DivByZero", func() {
Description("DivByZero is the error returned by the service methods when the right operand is 0.")
})
Method("integral_divide", func() {
// Method-specific definitions...
})
Method("divide", func() {
// Method-specific definitions...
})
})
In this example, we define a service-level error called DivByZero
that can
be used by any method within the divider
service. This is particularly useful
for common error conditions that might occur across multiple methods, such as
division by zero operations in this case.
Method-level errors are defined within the scope of a specific method and are only applicable to that method. This allows for more granular error handling tailored to individual operations.
var _ = Service("divider", func() {
Method("integral_divide", func() {
// The "HasRemainder" error is defined at the method
// level and is thus specific to "integral_divide".
Error("HasRemainder", func() {
Description("HasRemainder is the error returned when an integer division has a remainder.")
})
// Additional method definitions...
})
Method("divide", func() {
// Method-specific definitions...
})
})
In this example, we define a method-level error called HasRemainder
that is
specific to the integral_divide
method. This error would be used when the
division operation results in a remainder, which is particularly relevant for
integer division operations.
Goa allows you to reuse error definitions across multiple services and methods.
This is particularly useful for defining common errors that are used in multiple
parts of your API. Such definitions must appear in the API
DSL:
var _ = API("example", func() {
Error("NotFound", func() {
Description("Resource was not found in the system.")
})
HTTP(func() {
Response("NotFound", StatusNotFound)
})
GRPC(func() {
Response("NotFound", CodeNotFound)
})
})
var _ = Service("example", func() {
Method("get", func() {
Payload(func() {
Field(1, "id", String, "The ID of the concert to get.")
})
Result(Concert)
Error("NotFound")
HTTP(func() {
GET("/concerts/{id}")
})
GRPC(func() {})
})
})
In this example, we define a reusable error called NotFound
that can be used
by any method within the example
service. This error is defined in the API
DSL and is thus available to all services and methods within the API. The
NotFound
error is mapped to the HTTP status code 404
and the gRPC status
code NotFound
, this mapping is done in the API
DSL and does not need to be
repeated in the Service
or Method
DSLs.
The Error DSL in Goa provides several ways to customize how errors are defined and documented. You can specify descriptions, temporary/permanent status, and even define custom response structures.
The simplest form of error definition includes a name and description:
Error("NotFound", func() {
Description("Resource was not found in the system.")
})
The definition above is equivalent to:
Error("NotFound", ErrorResult, "Resource was not found in the system.")
The default type for errors is ErrorResult
which gets mapped to the
ServiceError type
in the generated code.
You can indicate whether an error is temporary, a timeout, or a fault - or any
combination of these using the Temporary
, Timeout
, and Fault
functions:
Error("ServiceUnavailable", func() {
Description("Service is temporarily unavailable.")
Temporary()
})
Error("RequestTimeout", func() {
Description("Request timed out.")
Timeout()
})
Error("InternalServerError", func() {
Description("Internal server error.")
Fault()
})
Clients can then lookup the corresponding fields from the ServiceError object to determine whether the error is temporary, a timeout, or a fault.
Note: this is only supported for
ErrorResult
errors for which the runtime type is ServiceError.
Goa also makes it easy to design custom error types, for example:
Error("ValidationError", DivByZero, "DivByZero is the error returned when using value 0 as divisor.")
This example assumes that DivByZero
is a custom error type defined elsewhere
in the file, for example:
var DivByZero = Type("DivByZero", func() {
Field(1, "name", String, "The name of the error.", func() {
Meta("struct:error:name")
})
Field(2, "message", String, "The error message.")
Required("name", "message")
})
These error definitions can be used at both service and method levels, providing flexibility in how you structure your API’s error handling. The Error DSL integrates with Goa’s code generation to produce consistent error responses across different transport protocols.
See Error Types for more details on custom error types.
Defining errors in Goa is a straightforward process that integrates seamlessly with your service design. By utilizing service-level and method-level error definitions, leveraging the default ErrorResult type, or creating custom error types, you can ensure that your APIs handle failures gracefully and communicate them effectively to clients. Proper error definitions not only enhance the robustness of your services but also improve the developer experience by providing clear and consistent error handling mechanisms.