Producing and consuming errors are essential aspects of error handling in Goa-based services. This section details how to generate errors within your service implementations and how to handle these errors on the client side effectively.
Goa generates helper functions for defined errors, simplifying the process of
creating standardized error responses. These helper functions ensure that errors
are consistent and properly formatted according to the service design. The
helper functions are named Make<ErrorName>
where <ErrorName>
is the name of
the error as defined in the DSL. They initialize the error fields based on the
service design (e.g. wh ether the error is a timeout, temporary, etc.).
Given the following service design:
var _ = Service("divider", func() {
Method("IntegralDivide", func() {
Payload(IntOperands)
Result(Int)
Error("DivByZero", ErrorResult, "Divisor cannot be zero")
Error("HasRemainder", ErrorResult, "Remainder is not zero")
HTTP(func() {
POST("/divide")
Response(StatusOK)
Response("DivByZero", StatusBadRequest)
Response("HasRemainder", StatusUnprocessableEntity)
})
})
})
var IntOperands = Type("IntOperands", func() {
Attribute("dividend", Int, "Dividend")
Attribute("divisor", Int, "Divisor")
Required("dividend", "divisor")
})
Example Implementation:
//...
func (s *dividerSvc) IntegralDivide(ctx context.Context, p *divider.IntOperands) (int, error) {
if p.Divisor == 0 {
return 0, gendivider.MakeDivByZero(fmt.Errorf("divisor cannot be zero"))
}
if p.Dividend%p.Divisor != 0 {
return 0, gendivider.MakeHasRemainder(fmt.Errorf("remainder is %d", p.Dividend%p.Divisor))
}
return p.Dividend / p.Divisor, nil
}
In this example:
gendivider
package is generated by Goa (under gen/divider
).MakeDivByZero
function creates a standardized DivByZero
error.MakeHasRemainder
function creates a standardized HasRemainder
error.These helper functions handle the initialization of error fields based on the
service design, ensuring that the errors are correctly serialized and mapped to
transport-specific status codes (400 for DivByZero
and 422 for
HasRemainder
in this example).
For more complex error scenarios, you might need to define custom error types.
Unlike the default ErrorResult
, custom error types allow you to include
additional contextual information relevant to the error.
Given the following service design:
var _ = Service("divider", func() {
Method("IntegralDivide", func() {
Payload(IntOperands)
Result(Int)
Error("DivByZero", DivByZero, "Divisor cannot be zero")
HTTP(func() {
POST("/divide")
Response(StatusOK)
Response("DivByZero", StatusBadRequest)
})
})
})
var DivByZero = Type("DivByZero", func() {
Description("DivByZero is the error returned when using value 0 as divisor.")
Field(1, "name", String, "Error name", func() {
Meta("struct:error:name")
})
Field(2, "message", String, "Error message for division by zero.")
Field(3, "dividend", Int, "Dividend that was used in the operation.")
Required("name", "message", "dividend")
})
Example Implementation:
func (s *dividerSvc) IntegralDivide(ctx context.Context, p *divider.IntOperands) (int, error) {
if p.Divisor == 0 {
return 0, &gendivider.DivByZero{Name: "DivByZero", Message: "divisor cannot be zero", Dividend: p.Dividend}
}
// Additional logic...
}
In this example:
DivByZero
struct is a custom error type defined in the service design.DivByZero
, you can provide custom detailed error
information.Meta("struct:error:name")
to allow Goa to correctly map the
error. This attribute must be set with the name of the error as defined in the
service design.Handling errors on the client side is as important as producing them on the server. Proper error handling ensures that clients can respond appropriately to different error scenarios.
When using the default ErrorResult
type, client-side errors are instances of
goa.ServiceError
. You can check the error type and handle it based on the
error name.
Example:
res, err := client.Divide(ctx, payload)
if err != nil {
if serr, ok := err.(*goa.ServiceError); ok {
switch serr.Name {
case "HasRemainder":
// Handle has remainder error
case "DivByZero":
// Handle division by zero error
default:
// Handle unknown errors
}
}
}
When using custom error types, client-side errors are instances of the corresponding generated Go structs. You can type assert the error to the specific custom type and handle it accordingly.
Example:
res, err := client.Divide(ctx, payload)
if err != nil {
if dbz, ok := err.(*gendivider.DivByZero); ok {
// Handle division by zero error
}
}
By effectively producing and consuming errors, you ensure that your Goa-based services communicate failures clearly and consistently. Utilizing generated helper functions for standard errors and custom error types for more complex scenarios allows for flexible and robust error handling. Proper client-side error handling further enhances the reliability and usability of your APIs, providing meaningful feedback to users and enabling appropriate corrective actions.
Testing error handling requires careful validation of error conditions and types. Here’s how to effectively test error handling using Clue’s mock package:
// Import Clue's mock package
import (
"github.com/goadesign/clue/mock"
)
// Mock implementation using Clue's mock package
// This shows how to properly structure a mock using Clue
type mockDividerService struct {
*mock.Mock // Embed Clue's Mock type
}
// IntegralDivide implements the mock using Clue's Next pattern
func (m *mockDividerService) IntegralDivide(ctx context.Context, p *divider.IntOperands) (int, error) {
if f := m.Next("IntegralDivide"); f != nil {
return f.(func(context.Context, *divider.IntOperands) (int, error))(ctx, p)
}
return 0, errors.New("unexpected call to IntegralDivide")
}
func TestIntegralDivide(t *testing.T) {
// Create mock service using Clue's mock package
// This demonstrates Clue's powerful mocking capabilities for testing
svc := &mockDividerService{mock.New()}
tests := []struct {
name string
setup func(*mockDividerService)
dividend int
divisor int
wantErr string
}{
{
name: "division by zero",
setup: func(m *mockDividerService) {
m.Set("IntegralDivide", func(ctx context.Context, p *divider.IntOperands) (int, error) {
if p.Divisor == 0 {
return 0, gendivider.MakeDivByZero(fmt.Errorf("divisor cannot be zero"))
}
return p.Dividend / p.Divisor, nil
})
},
dividend: 10,
divisor: 0,
wantErr: "divisor cannot be zero",
},
{
name: "has remainder",
setup: func(m *mockDividerService) {
m.Set("IntegralDivide", func(ctx context.Context, p *divider.IntOperands) (int, error) {
if p.Dividend%p.Divisor != 0 {
return 0, gendivider.MakeHasRemainder(fmt.Errorf("remainder is %d", p.Dividend%p.Divisor))
}
return p.Dividend / p.Divisor, nil
})
},
dividend: 10,
divisor: 3,
wantErr: "remainder is 1",
},
{
name: "successful division",
setup: func(m *mockDividerService) {
m.Set("IntegralDivide", func(ctx context.Context, p *divider.IntOperands) (int, error) {
return p.Dividend / p.Divisor, nil
})
},
dividend: 10,
divisor: 2,
wantErr: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create fresh mock for each test
mock := &mockDividerService{mock.New()}
if tt.setup != nil {
tt.setup(mock)
}
// Create payload
p := ÷r.IntOperands{
Dividend: tt.dividend,
Divisor: tt.divisor,
}
// Call service
result, err := mock.IntegralDivide(context.Background(), p)
// Verify error behavior
if tt.wantErr != "" {
if err == nil {
t.Errorf("expected error containing %q, got nil", tt.wantErr)
} else if !strings.Contains(err.Error(), tt.wantErr) {
t.Errorf("expected error containing %q, got %q", tt.wantErr, err.Error())
}
} else if err != nil {
t.Errorf("unexpected error: %v", err)
}
// For successful cases, verify the result
if tt.wantErr == "" && result != tt.dividend/tt.divisor {
t.Errorf("got result %d, want %d", result, tt.dividend/tt.divisor)
}
// Verify all expected calls were made
if mock.HasMore() {
t.Error("not all expected operations were performed")
}
})
}
}