An error type represents a specific failure condition with structured information. In a business layer, it lets validation report what went wrong without coupling domain code to gRPC status codes. section 10.3.1 section 10.7.3
Validate input with a typed error
Define an error carrying the invalid field and reason, then return it from validation: `type InvalidInputError struct { field, reason string }`; `func (e InvalidInputError) Error() string { return fmt.Sprintf("invalid input in field %s: %s", e.field, e.reason) }`. A validator can trim the name and return `InvalidInputError{field: "name", reason: "cannot be empty"}` when it remains blank. section 10.3.1
Use error types to preserve boundaries and make failures testable:
- Keep domain errors independent of transport details; let the API layer translate invalid input into gRPC code 3, INVALID_ARGUMENT. section 10.3.1
- Translate repository-specific errors with `errors.Is`; map a missing habit to `codes.NotFound` and unexpected failures to `codes.Internal`. section 10.7.3
- Test the concrete error with `assert.ErrorAs`, alongside valid complete and partially filled inputs. section 10.3.1
Listing 10.14 errors.go: Typed error for invalid input
// InvalidInputError is returned when user-input data is invalid.
type InvalidInputError struct {
field string
reason string #1
}
// Error implements error.
func (e InvalidInputError) Error() string {
return fmt.Sprintf("invalid input in field %s: %s", e.field, e.reason)
}
Inspect the complete typed error definition and its Error method. section 10.3.1
err := habit.Tick(ctx, s.db, s.db, habit.ID(request.HabitId), time.Now())
if err != nil {
switch {
case errors.Is(err, r.ErrNotFound):
return nil, status.Errorf(codes.NotFound,
"couldn't find habit %q in repository", request.HabitId)
default:
return nil, status.Errorf(codes.Internal,
"cannot tick habit %q: %s", request.HabitId, err.Error())
}
}
Inspect transport-level conversion from a domain error to a gRPC status. section 10.7.3




