Error Handling
Overview
Goa makes it possible to describe precisely the potential errors returned by service methods. This allows for defining a clear contract between the server and its clients that gets reflected in the generated documentation and code.
Goa takes a “battery included” approach where errors can be defined with as little information as just a name. However the DSL also makes it possible to describe completely new error types when Goa’s default is not sufficient.
Defining Errors
Errors are defined using the Error function:
var _ = Service("calc", func() {
Error("invalid_arguments")
})
Errors may also be defined within the scope of a specific method:
var _ = Service("calc", func() {
Method("divide", func() {
Payload(func() {
Field(1, "dividend", Int)
Field(1, "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
})
})
Both the invalid_arguments
and div_by_zero
errors in the examples above
make use of the default error type
ErrorResult.
Custom types can also be used when defining errors as follows:
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")
})
var _ = Service("calc", func() {
Method("divide", func() {
Payload(func() {
Field(1, "dividend", Int)
Field(1, "divisor", Int)
Required("dividend", "divisor")
})
Result(func() {
Field(1, "quotient", Int)
Field(2, "reminder", Int)
Required("quotient", "reminder")
})
Error("div_by_zero", DivByZero, "Division by zero") // Use DivByZero type to marshal error
})
})
If a type is used to define multiple errors it must define an attribute that
contains the error name so that the generated code can infer the
corresponding design definition. The attribute must be identified via the
struct:error:name
field
metadata, for example:
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() {
// Tell Goa to use the `name` field to match the error definition.
Meta("struct:error:name")
})
Required("message", "name")
})
The field must be initialized by the server code that returns the error. The generated code uses it to match the error definition and compute the proper status code.
Temporary Errors, Faults and Timeouts
The Error
function accepts an optional DSL function as last argument which
makes it possible to specify additional properties on the error. The Error
function DSL accepts three child functions:
Timeout()
identifies the error as being due to a server timeout.Fault()
identifies the error as a server side fault (e.g. a bug, a panic etc.).Temporary()
identifies the error as being retryable.
The following definition would be appropriate to define a timeout error:
Error("Timeout", ErrorResult, "Request timeout exceeded", func() {
Timeout()
})
The Timeout
, Fault
and Temporary
functions instruct the Goa code
generator to initialize the fields with the same names in the returned
ErrorResponse
object. They have no effect (other than documentation) when
using a custom error repsonse type.
Mapping Errors to Transport Status Codes
The Response function defines the HTTP or gRPC status code used to write the error:
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")
HTTP(func() {
POST("/")
Response("div_by_zero", StatusBadRequest, func() {
// Use HTTP status code 400 (BadRequest) to write "div_by_zero" errors
Description("Response used for division by zero errors")
})
})
GRPC(func() {
Response("div_by_zero", CodeInvalidArgument, func() {
// Use gRPC status code 3 (InvalidArgument) to write "div_by_zero" errors
Description("Response used for division by zero errors")
})
})
})
})
Producing Errors
Using the Default Error Type
With the design defined above Goa generates a helper function MakeDivByZero
that the server code may leverage to return errors. The function is generated
in the service specific package (under gen/calc
in this example). It
accepts a Go error as argument:
// Code generated by goa v....
// ...
package calc
// ...
// MakeDivByZero builds a goa.ServiceError from an error.
func MakeDivByZero(err error) *goa.ServiceError {
return &goa.ServiceError{
Name: "div_by_zero",
ID: goa.NewErrorID(),
Message: err.Error(),
}
}
// ...
This function can be used as follows when implementing the Divide
function:
func (s *calcsrvc) Divide(ctx context.Context, p *calc.DividePayload) (res *calc.DivideResult, err error) {
if p.Divisor == 0 {
return nil, calc.MakeDivByZero(fmt.Errorf("cannot divide by zero"))
}
// ...
}
The generated MakeXXX
functions create instances of the
ServiceError type.
Using Custom Error Types
When using a user defined type to define errors Goa does not generate helper
functions as it cannot tell how to map Go errors to the user type. Instead
the method implementation should instantiate the error type directly.
Considering the previous example using the DivByZero
type:
Error("div_by_zero", DivByZero, "Division by zero") // Use DivByZero type to marshal error
To return the error the method implementation should return an instance of
the generated DivByZero
struct also located in the service package (calc
in this example):
func (s *calcsrvc) Divide(ctx context.Context, p *calc.DividePayload) (res *calc.DivideResult, err error) {
if p.Divisor == 0 {
return nil, &calc.DivByZero{Message: "cannot divide by zero"}
}
// ...
}
Consuming Errors
The error values returned to the client are backed by the same structs used by the server to return errors.
Using the Default Error Type
If the error definition uses the default error type then the client side errors are instances of ServiceError:
// ... initialize endpoint, ctx, payload
c := calc.NewClient(endpoint)
res, err := c.Divide(ctx, payload)
if res != nil {
if dbz, ok := err.(*goa.ServiceError); ok {
// use dbz to handle error
}
}
// ...
Using Custom Error Types
If the error definition uses a custom type then the client side error is an instance of the corresponding generated Go struct:
// ... initialize endpoint, ctx, payload
c := calc.NewClient(endpoint)
res, err := c.Divide(ctx, payload)
if res != nil {
if dbz, ok := err.(*calc.DivByZero); ok {
// use dbz to handle error
}
}
// ...
Validation Errors
Validation errors are also instances of the
ServiceError struct.
The name
field of the struct makes it possible for client code to
differentiate between multiple possible errors.
Here is an example of how to do that which assumes that the design uses the
default error type to define the div_by_zero
error:
// ... initialize endpoint, ctx, payload
c := calc.NewClient(endpoint)
res, err := c.Divide(ctx, payload)
if res != nil {
if serr, ok := err.(*goa.ServiceError); ok {
switch serr.Name {
case "missing_field":
// Handle missing operand error
case "div_by_zero":
// Handle division by zero error
default:
// Handle unknown error
}
}
}
// ...
The validation error names are all defined in the file error.go, they are:
missing_payload
: error produced when a request is missing a required payload.decode_payload
: error produced when a request body cannot be decoded successfully.invalid_field_type
: error produced when the type of a payload field does not match the type defined in the design.missing_field
: error produced when a payload is missing a required field.invalid_enum_value
: error produced when the value of a payload field does not match one the values defined in the design Enum validation.invalid_format
: error produced the value of a payload field does not match the format validation defined in the design.invalid_pattern
: error produced when the value of a payload field does not match the pattern validation defined in the design.invalid_range
: error produced code when the value of a payload field does not match the range validation defined in the design.invalid_length
: error produced code when the value of a payload field does not match the length validation defined in the design.
Overridding Error Serialization
Sometimes it may be necessary to override the format used by the generated code to render validation errors. The generated HTTP handler and server creation functions make it possible to provide a custom error formatter function:
// Code generated by goa v...
package server
// ...
// New instantiates HTTP handlers for all the calc service endpoints using the
// provided encoder and decoder. The handlers are mounted on the given mux
// using the HTTP verb and path defined in the design. errhandler is called
// whenever a response fails to be encoded. formatter is used to format errors
// returned by the service methods prior to encoding. Both errhandler and
// formatter are optional and can be nil.
func New(
e *calc.Endpoints,
mux goahttp.Muxer,
decoder func(*http.Request) goahttp.Decoder,
encoder func(context.Context, http.ResponseWriter) goahttp.Encoder,
errhandler func(context.Context, http.ResponseWriter, error),
formatter func(context.Context, err error) goahttp.Statuser, // Error formatter function
// ...
The provided function must accept an instance of an error as argument and return a struct that implements the Statuser interface:
type Statuser interface {
// StatusCode return the HTTP status code used to encode the response
// when not defined in the design.
StatusCode() int
}
The generated code calls the struct StatusCode
method upon writing the HTTP
response and uses the return value to write the HTTP status code. The struct
is then serialized into the response body.
The default implementation used when the value nil
is given for the
formatter
argument of the New
function is
NewErrorResponse
which returns an instance of
ErrorResponse.
Overridding Validation Errors Serialization
A custom formatter can inspect the given error value similarly to how client code does it to format validation errors differently, for example:
// missingFieldError is the type used to serialize missing required field
// errors. It overrides the default provided by Goa.
type missingFieldError string
// StatusCode returns 400 (BadRequest).
func (missingFieldError) StatusCode() int { return http.StatusBadRequest }
// customErrorResponse converts err into a MissingField error if err corresponds
// to a missing required field validation error.
func customErrorResponse(ctx context.Context, err error) Statuser {
if serr, ok := err.(*goa.ServiceError); ok {
switch serr.Name {
case "missing_field":
return missingFieldError(serr.Message)
default:
// Use Goa default
return goahttp.NewErrorResponse(err)
}
}
// Use Goa default for all other error types
return goahttp.NewErrorResponse(err)
}
The custom formatter can then be used to instantiate a HTTP server or handler:
var (
calcServer *calcsvr.Server
)
{
eh := errorHandler(logger)
calcServer = calcsvr.New(calcEndpoints, mux, dec, enc, eh, customErrorResponse)
// ...
Example
The error example illustrates how to use custom error types and how to override the default error response used for validation errors.