This guide explains how to customize the way errors are serialized in Goa services. Error serialization is the process of converting error objects into a format that can be transmitted over HTTP or gRPC to clients. This is especially important for validation errors since they are automatically generated by Goa using a specific error type and cannot be customized at creation time - only their serialization can be controlled.
When an error occurs in your Goa service, it needs to be converted into a format that clients can
understand. The most common case where you need custom error formatting is for validation errors,
which are automatically generated by Goa and always use the ServiceError
type. You cannot change
how these errors are created, but you can control how they are formatted in the response.
Common scenarios where custom error formatting is needed:
Match your organization’s error format standards
Format validation errors consistently
ServiceError
)Provide custom error responses for specific error types
Goa uses the ServiceError
type internally for validation and other built-in
errors. This type includes several important fields:
// ServiceError is used by Goa for validation and other built-in errors
type ServiceError struct {
Name string // Error name (e.g., "missing_field")
ID string // Unique error ID
Field *string // Name of the field that caused the error when relevant
Message string // Human-readable error message
Timeout bool // Is this a timeout error?
Temporary bool // Is this a temporary error?
Fault bool // Is this a server fault?
}
By default, this gets serialized to JSON responses that look like this:
{
"name": "missing_field",
"id": "abc123",
"message": "email is missing from request body",
"field": "email"
}
To override the default error serialization, you need to provide a custom error formatter when creating the HTTP server.
The formatter must return a type that implements the Statuser
interface, which
requires a StatusCode()
method. This method determines the HTTP status code
that will be used in the response.
Here’s a detailed breakdown of how to implement custom error formatting:
// 1. Define your custom error response type
// This type determines the JSON structure of your error responses
type CustomErrorResponse struct {
// A machine-readable error code
Code string `json:"code"`
Message string `json:"message"`
Details map[string]string `json:"details,omitempty"`
}
// 2. Implement the Statuser interface
// This tells Goa what HTTP status code to use
func (r *CustomErrorResponse) StatusCode() int {
// You can implement complex logic here to determine the appropriate status code
switch r.Code {
case "VALIDATION_ERROR":
return http.StatusBadRequest
case "NOT_FOUND":
return http.StatusNotFound
default:
return http.StatusInternalServerError
}
}
// 3. Create a formatter function
// This function converts any error into your custom format
func customErrorFormatter(ctx context.Context, err error) goahttp.Statuser {
// Handle Goa's built-in ServiceError type (used for validation errors)
if serr, ok := err.(*goa.ServiceError); ok {
switch serr.Name {
// Common validation error cases
case goa.MissingField:
return &CustomErrorResponse{
Code: "MISSING_FIELD",
Message: fmt.Sprintf("The field '%s' is required", *serr.Field),
Details: map[string]string{
"field": *serr.Field,
},
}
case goa.InvalidFieldType:
return &CustomErrorResponse{
Code: "INVALID_TYPE",
Message: serr.Message,
Details: map[string]string{
"field": *serr.Field,
},
}
case goa.InvalidFormat:
resp.Details = map[string]string{
"field": *serr.Field,
"format_error": serr.Message,
}
// Handle other validation errors
default:
return &CustomErrorResponse{
Code: "VALIDATION_ERROR",
Message: serr.Message,
Details: map[string]string{
"error_id": serr.ID,
"field": *serr.Field,
},
}
}
}
// Handle other error types
return &CustomErrorResponse{
Code: "INTERNAL_ERROR",
Message: err.Error(),
}
}
// 4. Use the formatter when creating the server
var server *calcsvr.Server
{
// Create your error handler (for unexpected errors)
eh := errorHandler(logger)
// Create the server with your custom formatter
server = calcsvr.New(
endpoints, // Your service endpoints
mux, // The HTTP router
dec, // Request decoder
enc, // Response encoder
eh, // Error handler
customErrorFormatter, // Your custom formatter
)
}
This will produce JSON responses that look like:
{
"code": "MISSING_FIELD",
"message": "The field 'email' is required",
"details": {
"field": "email"
}
}
Consistent Format
Example of a consistent format:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid input provided",
"details": {
"field": "email",
"reason": "invalid_format",
"help": "Must be a valid email address"
},
"trace_id": "abc-123",
"timestamp": "2024-01-20T10:00:00Z"
}
}
Status Codes
Common status code usage:
func (r *CustomErrorResponse) StatusCode() int {
switch r.Code {
case "NOT_FOUND":
return http.StatusNotFound // 404
case "VALIDATION_ERROR":
return http.StatusBadRequest // 400
case "UNAUTHORIZED":
return http.StatusUnauthorized // 401
case "FORBIDDEN":
return http.StatusForbidden // 403
case "CONFLICT":
return http.StatusConflict // 409
case "INTERNAL_ERROR":
return http.StatusInternalServerError // 500
default:
return http.StatusInternalServerError
}
}
Security
Example of secure error handling:
func secureErrorFormatter(ctx context.Context, err error) goahttp.Statuser {
// Always log the full error details for debugging
log.Printf("Error: %+v", err)
if serr, ok := err.(*goa.ServiceError); ok {
switch serr.Name {
// Validation errors are safe to expose as they're user input problems
case goa.MissingField, goa.InvalidFieldType, goa.InvalidFormat,
goa.InvalidPattern, goa.InvalidRange, goa.InvalidLength:
return &CustomErrorResponse{
Code: "VALIDATION_ERROR",
Message: serr.Message,
Details: map[string]string{
"field": *serr.Field,
}
}
// Be careful with fault errors as they might expose internal details
case "internal_error":
if serr.Fault {
// Log for internal monitoring but return generic message
alertMonitoring(serr)
return &CustomErrorResponse{
Code: "INTERNAL_ERROR",
Message: "An internal error occurred",
}
}
// For temporary errors, indicate retry-ability but not the cause
case "service_unavailable":
if serr.Temporary {
return &CustomErrorResponse{
Code: "SERVICE_UNAVAILABLE",
Message: "Service temporarily unavailable",
Details: map[string]string{
"retry_after": "30",
},
}
}
}
}
// For any other errors, return a generic error response
// This prevents leaking internal implementation details
return &CustomErrorResponse{
Code: "UNEXPECTED_ERROR",
Message: "An unexpected error occurred",
}
}
Compatibility
Example of versioned error formats:
func versionedErrorFormatter(ctx context.Context, err error) goahttp.Statuser {
// Check API version from context
version := extractAPIVersion(ctx)
switch version {
case "v1":
return formatV1Error(err)
case "v2":
return formatV2Error(err)
default:
return formatLatestError(err)
}
}
Custom error serialization allows you to: