This example illustrates how to return and handle errors in Goa. Refer to Error Handling for additional information.
The example defines a calc service and two methods add and divide. The
service defines a timeout error which applies to both methods. The divide
method also defines a method specific div_by_zero error.
The timeout error does not specify a type and thus uses the default
(ErrorResult):
Error("timeout") // Use default error typeThe div_by_zero error specifies a custom error type DivByZero:
Error("div_by_zero", DivByZero, "division by 0") // Use custom error typeGiven the above Goa generates a function MakeTimeout and a Go struct
DivByZero both in the calc service Go package. The MakeTimeout function
makes it possible to return timeout errors given a Go error and is
leveraged by the service implementation to handle exceeded deadlines:
// Wait for results or timeout
select {
case <-ctx.Done():
// Timeout triggered, return timeout error
return nil, calc.MakeTimeout(ctx.Err())
case res := <-c:
// Division completed successfully
return res, nil
}The DivByZero struct can be used to return division by zero errors:
if p.Divisor == 0 {
return nil, &calc.DivByZero{Message: "divide by zero"}
}This example also illustrates how to override default validation error
responses. In this case the response returned for missing required field
errors. This is accomplished in the main package by providing a non-nil
error formatter function to the HTTP server New function:
calcServer = gencalcsvr.New(calcEndpoints, mux, dec, enc, eh, formatError)The formatError function is called by the generated code prior to writing
the HTTP response. The function takes the error as argument and returns the
value that should be serialized in the response body. The value returned by
the error formatter must implement the
Statuser interface
which defines a single method StatusCode() that returns the response
status code.
The example uses a string to send a missing-field error back. The formatter also preserves the response body declared for division by zero. All other errors use Goa's default response.
// missingFieldError is the type used to serialize missing required field
// errors. It overrides the default provided by Goa.
type missingFieldError string
// divByZeroError preserves the response declared for division by zero when the
// custom formatter handles all HTTP errors.
type divByZeroError struct {
Message string `form:"message" json:"message" xml:"message"`
}
func (missingFieldError) StatusCode() int { return http.StatusBadRequest }
func (*divByZeroError) StatusCode() int { return http.StatusBadRequest }
func formatError(ctx context.Context, err error) goahttp.Statuser {
var serviceError *goa.ServiceError
if errors.As(err, &serviceError) && serviceError.Name == "missing_field" {
return missingFieldError(serviceError.Message)
}
var divideError *gencalc.DivByZero
if errors.As(err, ÷Error) {
return &divByZeroError{Message: divideError.Message}
}
return goahttp.NewErrorResponse(ctx, err)
}First compile and start the server:
cd $GOPATH/src/goa.design/examples/error/cmd/calc
go build; ./calc --http-port 8080This shoud produce output similar to:
[calcapi] 11:50:56 HTTP "Divide" mounted on POST /
[calcapi] 11:50:56 serving gRPC method calc.Calc/Divide
[calcapi] 11:50:56 HTTP server listening on "localhost:8000"
[calcapi] 11:50:56 gRPC server listening on "localhost:8080"
In a different terminal, compile and run the client:
cd $GOPATH/src/goa.design/examples/error/cmd/calc-cli
go build; ./calc-cli -url http://localhost:8000 calc divide --body '{"dividend":1,"divisor":1}'
{
"Quotient": 1,
"Reminder": 0
}
./calc-cli -v -url http://localhost:8000 calc divide --body '{"dividend":1,"divisor":0}'
> POST http://localhost:8000/
{"dividend":1,"divisor":0}
< 400 Bad Request
< Content-Length: 29
< Content-Type: application/json
< Date: Sun, 22 Nov 2020 21:27:36 GMT
< Goa-Error: div_by_zero
{"message":"divide by zero"}
DivByZero is the error returned when using value 0 as divisor.Note that in that last example the message DivByZero is the error... is
produced by the generated client, the HTTP response is shown in the debug
output. The HTTP response body contains {"message":"divide by zero"} as
expected (a serialized DivByZero object). Now let's trigger a timeout
error:
./calc-cli -v -url http://localhost:8000 calc divide --body '{"dividend":42,"divisor":1}'
> POST http://localhost:8000/
{"dividend":42,"divisor":1}
< 504 Gateway Timeout
< Content-Length: 120
< Content-Type: application/json
< Date: Sun, 22 Nov 2020 21:33:37 GMT
< Goa-Error: timeout
{"name":"timeout","id":"_KL-qvO3","message":"context deadline exceeded","temporary":false,"timeout":true,"fault":false}
context deadline exceededHere we can see the server returning the default Goa error response type (and
as previously the message context deadline exceeded is produced by the
client). Note that the timeout field of the error response object is set to
true, this is because the design calls Timeout() in the timeout error
definition.
Finally we can check that the custom error type is used for missing required field errors. We use httpie to make the request since the generated client knows the field is required and would fail the request:
http POST localhost:8000 dividend:=42
HTTP/1.1 400 Bad Request
Content-Length: 35
Content-Type: application/json
Date: Mon, 23 Nov 2020 03:49:50 GMT
"\"divisor\" is missing from body"The response body consists of a string instead of the default Goa error response object.
Consult the error handling section of goa.design to learn more about error handling in Goa.