Handling Errors
Goa makes it possible to describe the errors that a service method may return. From this description Goa can generate both code and documentation. The code provides the transport specific marshalling and unmarshalling logic. Errors have a name, a type which may be a primitive type or a user defined type and a description that is used to generate comments and documentation.
This document describes how to define errors in Goa designs and how to leverage the generated code to return errors from service methods.
Design
The Goa DSL makes it possible to define error results on methods and on entire services using the Error expression:
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() {
// 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.")
})
// ...
})
Method("divide", func() {
// ...
})
})
In this example both the DivByZero
and HasRemainder
errors use the default
error type ErrorResult.
This type defines the following fields:
Name
is the name of the error. The generated code takes care of initializing the field with the name defined in the design during response encoding.ID
is a unique identifier for the specific instance of the error. The idea is that this ID may be instrumented making it possible to correlate a user error report with service logs, traces etc.Message
is the error message.Temporary
indicates whether the error is temporary.Timeout
indicates whether the error is due to a timeout.Fault
indicates whether the error is due to a server-side fault.
The DSL makes it possible to specify whether an error denotes a temporary condition and/or a timeout or a server-side fault.
Error("network_failure", func() {
Temporary()
})
Error("timeout"), func() {
Timeout()
})
Error("remote_timeout", func() {
Temporary()
Timeout()
})
Error("internal_error", func() {
Fault()
})
The generated code takes care of initializing the ErrorResult
with
Temporary
, Timeout
and Fault
fields appropriately when encoding the
error response.
Designing Responses
The Response function makes it possible to define the HTTP/gRPC responses associated with a given error.
var _ = Service("divider", func() {
Error("DivByZero")
HTTP(func() {
// Use HTTP status code 400 Bad Request for "DivByZero"
// errors.
Response("DivByZero", StatusBadRequest)
})
GRPC(func() {
// Use gRPC status code "InvalidArgument" for "DivByZero"
// errors.
Response("DivByZero", CodeInvalidArgument)
})
Method("integral_divide", func() {
Error("HasRemainder")
HTTP(func() {
Response("HasRemainder", StatusExpectationFailed)
// ...
})
GRPC(func() {
Response("HasRemainder", CodeUnknown)
})
})
// ...
})
Return Errors
Given the divider service design above goa generates two helper functions that
build the corresponding errors: MakeDivByZero
and MakeHasRemainder
. These
functions accept a Go error as argument making it convenient to map a business
logic error to a specific error result.
Here is an example of what an implementation of integral_divide
could look
like:
func (s *dividerSvc) IntegralDivide(ctx context.Context, p *dividersvc.IntOperands) (int, error) {
if p.B == 0 {
// Use generated function to create error result
return 0, dividersvc.MakeDivByZero(fmt.Errorf("right operand cannot be 0"))
}
if p.A%p.B != 0 {
return 0, dividersvc.MakeHasRemainder(fmt.Errorf("remainder is %d", p.A%p.B))
}
return p.A / p.B, nil
}
And that’s it! Given this, goa knows to initialize a ErrorResult
using the
provided error to initialize the message field and initializes all the other
fields from the information provided in the design. The generated transport code
also writes the proper HTTP/gRPC status code as defined in the Response DSL.
Using the generated command line tool to verify:
./dividercli -v divider integer-divide -a 1 -b 2
> GET http://localhost:8080/idiv/1/2
< 417 Expectation Failed
< Content-Length: 68
< Content-Type: application/json
< Date: Thu, 22 Mar 2018 01:34:33 GMT
{"name":"HasRemainder","id":"dlqvenWL","message":"remainder is 1"}
Using Custom Error Types
The Error
DSL allows specifying a type for the error result thereby
overridding the default consisting of ErrorResult
. Any type can be used for
defining the shape of the error result. Here is an example using string as error
result type:
Error("NotFound", String, "NotFound is the error returned when there is no bottle with the given ID.")
Note how the description can be defined inline when the type is defined
explicitly. The type may be ErrorResult
which makes it possible to define
a description inline in this case as well.
There are a couple of caveats to be aware of when using custom error result types:
The
Temporary
,Timeout
, andFault
expressions have no effect on code generation in this case as they otherwise set the corresponding field values on theErrorResult
struct.If the custom type is a user defined type and if it is used to define multiple errors on the same method then goa must be told which attribute contains the error name. The value of this attribute is compared with the names of the errors as defined in the design by the encoding and decoding code to infer the proper encoding details (e.g. HTTP status code). The attribute is identified using the special
struct:error:name
meta, must be a string and must be required:
var InsertConflict = ResultType("application/vnd.service.insertconflict", func() {
Description("InsertConflict is the type of the error values returned when insertion fails because of a conflict")
Attributes(func() {
Attribute("conflict_value", String)
Attribute("name", String, "name of error used by goa to encode response", func() {
Meta("struct:error:name")
})
Required("conflict_value", "name")
})
View("default", func() {
Attribute("conflict_value")
// Note: error_name is omitted from the default view.
// in this example error_name is an attribute used to identify
// the field containing the name of the error and is not
// encoded in the response sent to the client.
})
})
- User types used to define custom error types cannot have an attribute named
error_name
as the generated code defines aErrorName
function on the error struct.