Loading documentation...
Loading documentation...
Loading documentation...
Note: This is a developer-maintained documentation page. The content here is not auto-generated and should be updated manually to explain the core concepts and architecture of error handling in Helix.
Helix uses RFC 7807 Problem Details for HTTP APIs as the standard format for error responses. This provides a consistent, machine-readable way to communicate errors that integrates seamlessly with modern API clients.
RFC 7807 defines a standard JSON format for HTTP API errors. Helix implements this standard with extensions for validation errors.
A Problem response contains:
errorProblemProblemtype Problem struct {
Type string `json:"type"`
Title string `json:"title"`
Status int `json:"status"`
Detail string `json:"detail,omitempty"`
Instance string `json:"instance,omitempty"`
Err error `json:"-"` // Original error (not serialized)
}Helix provides pre-defined Problem values for common HTTP errors:
ErrBadRequest (400)ErrUnauthorized (401)ErrForbidden (403)ErrNotFound (404)ErrMethodNotAllowed (405)ErrConflict (409)ErrGone (410)ErrUnprocessableEntity (422)ErrTooManyRequests (429)ErrInternal (500)ErrNotImplemented (501)ErrBadGateway (502)ErrServiceUnavailable (503)ErrGatewayTimeout (504)Handlers return error, which is automatically converted to Problem responses:
s.GET("/users/{id}", helix.HandleCtx(func(c *helix.Ctx) error {
id := c.Param("id")
user, err := userService.Get(c.Context(), id)
if err != nil {
return helix.NotFoundf("user %s not found", id)
}
return c.OK(user)
}))Use pre-defined errors for common cases:
// Simple error
return helix.ErrNotFound
// With detail
return helix.ErrNotFound.WithDetail("user 123 not found")
// With formatted detail
return helix.NotFoundf("user %d not found", id)Create custom problems for domain-specific errors:
var ErrDuplicateEmail = helix.NewProblem(
http.StatusConflict,
"duplicate_email",
"Email Already Exists",
)
// In handler
if exists {
return ErrDuplicateEmail.WithDetailf("email %s is already registered", email)
}Validation errors are automatically converted to RFC 7807 with field-level details:
type CreateUserRequest struct {
Name string `json:"name"`
Email string `json:"email"`
}
func (r *CreateUserRequest) Validate() error {
v := helix.NewValidationErrors()
if r.Name == "" {
v.Add("name", "name is required")
}
if r.Email == "" {
v.Add("email", "email is required")
} else if !strings.Contains(r.Email, "@") {
v.Add("email", "invalid email format")
}
return v.Err() // Returns nil if no errors
}
// Handler automatically gets validation errors
s.POST("/users", helix.Handle(func(ctx context.Context, req CreateUserRequest) (User, error) {
// Validation happens automatically if req implements Validatable
return userService.Create(ctx, req)
}))Validation errors include an errors array with field-level details:
{
"type": "about:blank#unprocessable_entity",
"title": "Unprocessable Entity",
"status": 422,
"detail": "One or more validation errors occurred",
"instance": "/users",
"errors": [
{
"field": "name",
"message": "name is required"
},
{
"field": "email",
"message": "invalid email format"
}
]
}s.GET("/users/{id}", helix.HandleCtx(func(c *helix.Ctx) error {
id, err := c.ParamInt("id")
if err != nil {
return helix.BadRequestf("invalid user id: %v", err)
}
user, err := userService.Get(c.Context(), id)
if err == ErrUserNotFound {
return helix.NotFoundf("user %d not found", id)
}
if err != nil {
return err // Wrapped as ErrInternal
}
return c.OK(user)
}))Wrap errors with context for better debugging:
user, err := userService.Get(ctx, id)
if err != nil {
return fmt.Errorf("failed to get user %d: %w", id, err)
// Automatically converted to ErrInternal with detail
}Define domain errors that map to HTTP problems:
var (
ErrUserNotFound = helix.NewProblem(
http.StatusNotFound,
"user_not_found",
"User Not Found",
)
ErrEmailExists = helix.NewProblem(
http.StatusConflict,
"email_exists",
"Email Already Exists",
)
ErrInvalidCredentials = helix.NewProblem(
http.StatusUnauthorized,
"invalid_credentials",
"Invalid Credentials",
)
)
// In service layer
func (s *UserService) Get(ctx context.Context, id int) (User, error) {
user, err := s.repo.FindByID(ctx, id)
if err == sql.ErrNoRows {
return User{}, ErrUserNotFound.WithDetailf("user %d not found", id)
}
return user, err
}Middleware can handle errors before they reach handlers:
func errorLoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Wrap response writer to capture errors
rw := &errorResponseWriter{ResponseWriter: w}
next.ServeHTTP(rw, r)
if rw.err != nil {
logs.ErrorContext(r.Context(), "request error",
logs.Err(rw.err),
logs.String("path", r.URL.Path),
logs.Int("status", rw.status),
)
}
})
}Helix uses RFC 7807 because:
Errors are automatically converted to Problems because:
Validation errors extend RFC 7807 with an errors array because:
Problem: If a handler doesn't return an error, the response may not be sent correctly.
Solution: Always return errors (or nil):
// ❌ Wrong
s.GET("/users", helix.HandleCtx(func(c *helix.Ctx) error {
users, _ := userService.List(c.Context())
c.OK(users) // Missing return
}))
// ✅ Correct
s.GET("/users", helix.HandleCtx(func(c *helix.Ctx) error {
users, err := userService.List(c.Context())
if err != nil {
return err
}
return c.OK(users)
}))Problem: Returning raw database or internal errors exposes implementation details.
Solution: Wrap errors with user-friendly messages:
// ❌ Wrong - exposes database error
user, err := db.Query("SELECT...")
if err != nil {
return err // "pq: relation 'users' does not exist"
}
// ✅ Correct - user-friendly error
user, err := db.Query("SELECT...")
if err != nil {
logs.ErrorContext(ctx, "database error", logs.Err(err))
return helix.ErrInternal.WithDetail("failed to retrieve user")
}Problem: Custom error handlers must handle all error types.
Solution: Always include a default case:
func errorHandler(w http.ResponseWriter, r *http.Request, err error) {
switch e := err.(type) {
case helix.Problem:
helix.WriteProblem(w, e)
case *helix.ValidationErrors:
helix.WriteValidationProblem(w, e)
default:
// Always handle unknown errors
logs.ErrorContext(r.Context(), "unhandled error", logs.Err(err))
helix.WriteProblem(w, helix.ErrInternal.WithDetail("an error occurred"))
}
}Log errors with context:
import "github.com/kolosys/helix/logs"
s.GET("/users/{id}", helix.HandleCtx(func(c *helix.Ctx) error {
user, err := userService.Get(c.Context(), c.Param("id"))
if err != nil {
logs.ErrorContext(c.Context(), "failed to get user",
logs.Err(err),
logs.String("user_id", c.Param("id")),
)
return helix.NotFoundf("user not found")
}
return c.OK(user)
}))Track error rates and types:
func errorTrackingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rw := &trackingResponseWriter{ResponseWriter: w}
next.ServeHTTP(rw, r)
if rw.status >= 400 {
metrics.IncrementErrorCounter(r.Method, r.URL.Path, rw.status)
}
})
}Replace default error handling:
func apiErrorHandler(w http.ResponseWriter, r *http.Request, err error) {
var problem helix.Problem
switch e := err.(type) {
case helix.Problem:
problem = e
case *helix.ValidationErrors:
helix.WriteValidationProblem(w, e)
return
default:
// Log unexpected errors
logs.ErrorContext(r.Context(), "unexpected error", logs.Err(err))
problem = helix.ErrInternal.WithDetail("an error occurred")
}
// Add request ID to problem
if requestID := r.Header.Get("X-Request-ID"); requestID != "" {
problem = problem.WithDetailf("%s (request-id: %s)", problem.Detail, requestID)
}
helix.WriteProblem(w, problem)
}
s := helix.New(helix.WithErrorHandler(apiErrorHandler))This documentation should be updated by package maintainers to reflect the actual architecture and design patterns used.
Note: This is a developer-maintained documentation page. The content here is not auto-generated and should be updated manually to explain the core concepts and architecture of error handling in Helix.
Helix uses RFC 7807 Problem Details for HTTP APIs as the standard format for error responses. This provides a consistent, machine-readable way to communicate errors that integrates seamlessly with modern API clients.
RFC 7807 defines a standard JSON format for HTTP API errors. Helix implements this standard with extensions for validation errors.
A Problem response contains:
errorProblemProblemtype Problem struct {
Type string `json:"type"`
Title string `json:"title"`
Status int `json:"status"`
Detail string `json:"detail,omitempty"`
Instance string `json:"instance,omitempty"`
Err error `json:"-"` // Original error (not serialized)
}Helix provides pre-defined Problem values for common HTTP errors:
ErrBadRequest (400)ErrUnauthorized (401)ErrForbidden (403)ErrNotFound (404)ErrMethodNotAllowed (405)ErrConflict (409)ErrGone (410)ErrUnprocessableEntity (422)ErrTooManyRequests (429)ErrInternal (500)ErrNotImplemented (501)ErrBadGateway (502)ErrServiceUnavailable (503)ErrGatewayTimeout (504)Handlers return error, which is automatically converted to Problem responses:
s.GET("/users/{id}", helix.HandleCtx(func(c *helix.Ctx) error {
id := c.Param("id")
user, err := userService.Get(c.Context(), id)
if err != nil {
return helix.NotFoundf("user %s not found", id)
}
return c.OK(user)
}))Use pre-defined errors for common cases:
// Simple error
return helix.ErrNotFound
// With detail
return helix.ErrNotFound.WithDetail("user 123 not found")
// With formatted detail
return helix.NotFoundf("user %d not found", id)Create custom problems for domain-specific errors:
var ErrDuplicateEmail = helix.NewProblem(
http.StatusConflict,
"duplicate_email",
"Email Already Exists",
)
// In handler
if exists {
return ErrDuplicateEmail.WithDetailf("email %s is already registered", email)
}Validation errors are automatically converted to RFC 7807 with field-level details:
type CreateUserRequest struct {
Name string `json:"name"`
Email string `json:"email"`
}
func (r *CreateUserRequest) Validate() error {
v := helix.NewValidationErrors()
if r.Name == "" {
v.Add("name", "name is required")
}
if r.Email == "" {
v.Add("email", "email is required")
} else if !strings.Contains(r.Email, "@") {
v.Add("email", "invalid email format")
}
return v.Err() // Returns nil if no errors
}
// Handler automatically gets validation errors
s.POST("/users", helix.Handle(func(ctx context.Context, req CreateUserRequest) (User, error) {
// Validation happens automatically if req implements Validatable
return userService.Create(ctx, req)
}))Validation errors include an errors array with field-level details:
{
"type": "about:blank#unprocessable_entity",
"title": "Unprocessable Entity",
"status": 422,
"detail": "One or more validation errors occurred",
"instance": "/users",
"errors": [
{
"field": "name",
"message": "name is required"
},
{
"field": "email",
"message": "invalid email format"
}
]
}s.GET("/users/{id}", helix.HandleCtx(func(c *helix.Ctx) error {
id, err := c.ParamInt("id")
if err != nil {
return helix.BadRequestf("invalid user id: %v", err)
}
user, err := userService.Get(c.Context(), id)
if err == ErrUserNotFound {
return helix.NotFoundf("user %d not found", id)
}
if err != nil {
return err // Wrapped as ErrInternal
}
return c.OK(user)
}))Wrap errors with context for better debugging:
user, err := userService.Get(ctx, id)
if err != nil {
return fmt.Errorf("failed to get user %d: %w", id, err)
// Automatically converted to ErrInternal with detail
}Define domain errors that map to HTTP problems:
var (
ErrUserNotFound = helix.NewProblem(
http.StatusNotFound,
"user_not_found",
"User Not Found",
)
ErrEmailExists = helix.NewProblem(
http.StatusConflict,
"email_exists",
"Email Already Exists",
)
ErrInvalidCredentials = helix.NewProblem(
http.StatusUnauthorized,
"invalid_credentials",
"Invalid Credentials",
)
)
// In service layer
func (s *UserService) Get(ctx context.Context, id int) (User, error) {
user, err := s.repo.FindByID(ctx, id)
if err == sql.ErrNoRows {
return User{}, ErrUserNotFound.WithDetailf("user %d not found", id)
}
return user, err
}Middleware can handle errors before they reach handlers:
func errorLoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Wrap response writer to capture errors
rw := &errorResponseWriter{ResponseWriter: w}
next.ServeHTTP(rw, r)
if rw.err != nil {
logs.ErrorContext(r.Context(), "request error",
logs.Err(rw.err),
logs.String("path", r.URL.Path),
logs.Int("status", rw.status),
)
}
})
}Helix uses RFC 7807 because:
Errors are automatically converted to Problems because:
Validation errors extend RFC 7807 with an errors array because:
Problem: If a handler doesn't return an error, the response may not be sent correctly.
Solution: Always return errors (or nil):
// ❌ Wrong
s.GET("/users", helix.HandleCtx(func(c *helix.Ctx) error {
users, _ := userService.List(c.Context())
c.OK(users) // Missing return
}))
// ✅ Correct
s.GET("/users", helix.HandleCtx(func(c *helix.Ctx) error {
users, err := userService.List(c.Context())
if err != nil {
return err
}
return c.OK(users)
}))Problem: Returning raw database or internal errors exposes implementation details.
Solution: Wrap errors with user-friendly messages:
// ❌ Wrong - exposes database error
user, err := db.Query("SELECT...")
if err != nil {
return err // "pq: relation 'users' does not exist"
}
// ✅ Correct - user-friendly error
user, err := db.Query("SELECT...")
if err != nil {
logs.ErrorContext(ctx, "database error", logs.Err(err))
return helix.ErrInternal.WithDetail("failed to retrieve user")
}Problem: Custom error handlers must handle all error types.
Solution: Always include a default case:
func errorHandler(w http.ResponseWriter, r *http.Request, err error) {
switch e := err.(type) {
case helix.Problem:
helix.WriteProblem(w, e)
case *helix.ValidationErrors:
helix.WriteValidationProblem(w, e)
default:
// Always handle unknown errors
logs.ErrorContext(r.Context(), "unhandled error", logs.Err(err))
helix.WriteProblem(w, helix.ErrInternal.WithDetail("an error occurred"))
}
}Log errors with context:
import "github.com/kolosys/helix/logs"
s.GET("/users/{id}", helix.HandleCtx(func(c *helix.Ctx) error {
user, err := userService.Get(c.Context(), c.Param("id"))
if err != nil {
logs.ErrorContext(c.Context(), "failed to get user",
logs.Err(err),
logs.String("user_id", c.Param("id")),
)
return helix.NotFoundf("user not found")
}
return c.OK(user)
}))Track error rates and types:
func errorTrackingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rw := &trackingResponseWriter{ResponseWriter: w}
next.ServeHTTP(rw, r)
if rw.status >= 400 {
metrics.IncrementErrorCounter(r.Method, r.URL.Path, rw.status)
}
})
}Replace default error handling:
func apiErrorHandler(w http.ResponseWriter, r *http.Request, err error) {
var problem helix.Problem
switch e := err.(type) {
case helix.Problem:
problem = e
case *helix.ValidationErrors:
helix.WriteValidationProblem(w, e)
return
default:
// Log unexpected errors
logs.ErrorContext(r.Context(), "unexpected error", logs.Err(err))
problem = helix.ErrInternal.WithDetail("an error occurred")
}
// Add request ID to problem
if requestID := r.Header.Get("X-Request-ID"); requestID != "" {
problem = problem.WithDetailf("%s (request-id: %s)", problem.Detail, requestID)
}
helix.WriteProblem(w, problem)
}
s := helix.New(helix.WithErrorHandler(apiErrorHandler))This documentation should be updated by package maintainers to reflect the actual architecture and design patterns used.
type Problem struct {
Type string `json:"type"`
Title string `json:"title"`
Status int `json:"status"`
Detail string `json:"detail,omitempty"`
Instance string `json:"instance,omitempty"`
Err error `json:"-"` // Original error (not serialized)
}s.GET("/users/{id}", helix.HandleCtx(func(c *helix.Ctx) error {
id := c.Param("id")
user, err := userService.Get(c.Context(), id)
if err != nil {
return helix.NotFoundf("user %s not found", id)
}
return c.OK(user)
}))// Simple error
return helix.ErrNotFound
// With detail
return helix.ErrNotFound.WithDetail("user 123 not found")
// With formatted detail
return helix.NotFoundf("user %d not found", id)var ErrDuplicateEmail = helix.NewProblem(
http.StatusConflict,
"duplicate_email",
"Email Already Exists",
)
// In handler
if exists {
return ErrDuplicateEmail.WithDetailf("email %s is already registered", email)
}type CreateUserRequest struct {
Name string `json:"name"`
Email string `json:"email"`
}
func (r *CreateUserRequest) Validate() error {
v := helix.NewValidationErrors()
if r.Name == "" {
v.Add("name", "name is required")
}
if r.Email == "" {
v.Add("email", "email is required")
} else if !strings.Contains(r.Email, "@") {
v.Add("email", "invalid email format")
}
return v.Err() // Returns nil if no errors
}
// Handler automatically gets validation errors
s.POST("/users", helix.Handle(func(ctx context.Context, req CreateUserRequest) (User, error) {
// Validation happens automatically if req implements Validatable
return userService.Create(ctx, req)
})){
"type": "about:blank#unprocessable_entity",
"title": "Unprocessable Entity",
"status": 422,
"detail": "One or more validation errors occurred",
"instance": "/users",
"errors": [
{
"field": "name",
"message": "name is required"
},
{
"field": "email",
"message": "invalid email format"
}
]
}s.GET("/users/{id}", helix.HandleCtx(func(c *helix.Ctx) error {
id, err := c.ParamInt("id")
if err != nil {
return helix.BadRequestf("invalid user id: %v", err)
}
user, err := userService.Get(c.Context(), id)
if err == ErrUserNotFound {
return helix.NotFoundf("user %d not found", id)
}
if err != nil {
return err // Wrapped as ErrInternal
}
return c.OK(user)
}))user, err := userService.Get(ctx, id)
if err != nil {
return fmt.Errorf("failed to get user %d: %w", id, err)
// Automatically converted to ErrInternal with detail
}var (
ErrUserNotFound = helix.NewProblem(
http.StatusNotFound,
"user_not_found",
"User Not Found",
)
ErrEmailExists = helix.NewProblem(
http.StatusConflict,
"email_exists",
"Email Already Exists",
)
ErrInvalidCredentials = helix.NewProblem(
http.StatusUnauthorized,
"invalid_credentials",
"Invalid Credentials",
)
)
// In service layer
func (s *UserService) Get(ctx context.Context, id int) (User, error) {
user, err := s.repo.FindByID(ctx, id)
if err == sql.ErrNoRows {
return User{}, ErrUserNotFound.WithDetailf("user %d not found", id)
}
return user, err
}func errorLoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Wrap response writer to capture errors
rw := &errorResponseWriter{ResponseWriter: w}
next.ServeHTTP(rw, r)
if rw.err != nil {
logs.ErrorContext(r.Context(), "request error",
logs.Err(rw.err),
logs.String("path", r.URL.Path),
logs.Int("status", rw.status),
)
}
})
}// ❌ Wrong
s.GET("/users", helix.HandleCtx(func(c *helix.Ctx) error {
users, _ := userService.List(c.Context())
c.OK(users) // Missing return
}))
// ✅ Correct
s.GET("/users", helix.HandleCtx(func(c *helix.Ctx) error {
users, err := userService.List(c.Context())
if err != nil {
return err
}
return c.OK(users)
}))// ❌ Wrong - exposes database error
user, err := db.Query("SELECT...")
if err != nil {
return err // "pq: relation 'users' does not exist"
}
// ✅ Correct - user-friendly error
user, err := db.Query("SELECT...")
if err != nil {
logs.ErrorContext(ctx, "database error", logs.Err(err))
return helix.ErrInternal.WithDetail("failed to retrieve user")
}func errorHandler(w http.ResponseWriter, r *http.Request, err error) {
switch e := err.(type) {
case helix.Problem:
helix.WriteProblem(w, e)
case *helix.ValidationErrors:
helix.WriteValidationProblem(w, e)
default:
// Always handle unknown errors
logs.ErrorContext(r.Context(), "unhandled error", logs.Err(err))
helix.WriteProblem(w, helix.ErrInternal.WithDetail("an error occurred"))
}
}import "github.com/kolosys/helix/logs"
s.GET("/users/{id}", helix.HandleCtx(func(c *helix.Ctx) error {
user, err := userService.Get(c.Context(), c.Param("id"))
if err != nil {
logs.ErrorContext(c.Context(), "failed to get user",
logs.Err(err),
logs.String("user_id", c.Param("id")),
)
return helix.NotFoundf("user not found")
}
return c.OK(user)
}))func errorTrackingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rw := &trackingResponseWriter{ResponseWriter: w}
next.ServeHTTP(rw, r)
if rw.status >= 400 {
metrics.IncrementErrorCounter(r.Method, r.URL.Path, rw.status)
}
})
}func apiErrorHandler(w http.ResponseWriter, r *http.Request, err error) {
var problem helix.Problem
switch e := err.(type) {
case helix.Problem:
problem = e
case *helix.ValidationErrors:
helix.WriteValidationProblem(w, e)
return
default:
// Log unexpected errors
logs.ErrorContext(r.Context(), "unexpected error", logs.Err(err))
problem = helix.ErrInternal.WithDetail("an error occurred")
}
// Add request ID to problem
if requestID := r.Header.Get("X-Request-ID"); requestID != "" {
problem = problem.WithDetailf("%s (request-id: %s)", problem.Detail, requestID)
}
helix.WriteProblem(w, problem)
}
s := helix.New(helix.WithErrorHandler(apiErrorHandler))type Problem struct {
Type string `json:"type"`
Title string `json:"title"`
Status int `json:"status"`
Detail string `json:"detail,omitempty"`
Instance string `json:"instance,omitempty"`
Err error `json:"-"` // Original error (not serialized)
}s.GET("/users/{id}", helix.HandleCtx(func(c *helix.Ctx) error {
id := c.Param("id")
user, err := userService.Get(c.Context(), id)
if err != nil {
return helix.NotFoundf("user %s not found", id)
}
return c.OK(user)
}))// Simple error
return helix.ErrNotFound
// With detail
return helix.ErrNotFound.WithDetail("user 123 not found")
// With formatted detail
return helix.NotFoundf("user %d not found", id)var ErrDuplicateEmail = helix.NewProblem(
http.StatusConflict,
"duplicate_email",
"Email Already Exists",
)
// In handler
if exists {
return ErrDuplicateEmail.WithDetailf("email %s is already registered", email)
}type CreateUserRequest struct {
Name string `json:"name"`
Email string `json:"email"`
}
func (r *CreateUserRequest) Validate() error {
v := helix.NewValidationErrors()
if r.Name == "" {
v.Add("name", "name is required")
}
if r.Email == "" {
v.Add("email", "email is required")
} else if !strings.Contains(r.Email, "@") {
v.Add("email", "invalid email format")
}
return v.Err() // Returns nil if no errors
}
// Handler automatically gets validation errors
s.POST("/users", helix.Handle(func(ctx context.Context, req CreateUserRequest) (User, error) {
// Validation happens automatically if req implements Validatable
return userService.Create(ctx, req)
})){
"type": "about:blank#unprocessable_entity",
"title": "Unprocessable Entity",
"status": 422,
"detail": "One or more validation errors occurred",
"instance": "/users",
"errors": [
{
"field": "name",
"message": "name is required"
},
{
"field": "email",
"message": "invalid email format"
}
]
}s.GET("/users/{id}", helix.HandleCtx(func(c *helix.Ctx) error {
id, err := c.ParamInt("id")
if err != nil {
return helix.BadRequestf("invalid user id: %v", err)
}
user, err := userService.Get(c.Context(), id)
if err == ErrUserNotFound {
return helix.NotFoundf("user %d not found", id)
}
if err != nil {
return err // Wrapped as ErrInternal
}
return c.OK(user)
}))user, err := userService.Get(ctx, id)
if err != nil {
return fmt.Errorf("failed to get user %d: %w", id, err)
// Automatically converted to ErrInternal with detail
}var (
ErrUserNotFound = helix.NewProblem(
http.StatusNotFound,
"user_not_found",
"User Not Found",
)
ErrEmailExists = helix.NewProblem(
http.StatusConflict,
"email_exists",
"Email Already Exists",
)
ErrInvalidCredentials = helix.NewProblem(
http.StatusUnauthorized,
"invalid_credentials",
"Invalid Credentials",
)
)
// In service layer
func (s *UserService) Get(ctx context.Context, id int) (User, error) {
user, err := s.repo.FindByID(ctx, id)
if err == sql.ErrNoRows {
return User{}, ErrUserNotFound.WithDetailf("user %d not found", id)
}
return user, err
}func errorLoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Wrap response writer to capture errors
rw := &errorResponseWriter{ResponseWriter: w}
next.ServeHTTP(rw, r)
if rw.err != nil {
logs.ErrorContext(r.Context(), "request error",
logs.Err(rw.err),
logs.String("path", r.URL.Path),
logs.Int("status", rw.status),
)
}
})
}// ❌ Wrong
s.GET("/users", helix.HandleCtx(func(c *helix.Ctx) error {
users, _ := userService.List(c.Context())
c.OK(users) // Missing return
}))
// ✅ Correct
s.GET("/users", helix.HandleCtx(func(c *helix.Ctx) error {
users, err := userService.List(c.Context())
if err != nil {
return err
}
return c.OK(users)
}))// ❌ Wrong - exposes database error
user, err := db.Query("SELECT...")
if err != nil {
return err // "pq: relation 'users' does not exist"
}
// ✅ Correct - user-friendly error
user, err := db.Query("SELECT...")
if err != nil {
logs.ErrorContext(ctx, "database error", logs.Err(err))
return helix.ErrInternal.WithDetail("failed to retrieve user")
}func errorHandler(w http.ResponseWriter, r *http.Request, err error) {
switch e := err.(type) {
case helix.Problem:
helix.WriteProblem(w, e)
case *helix.ValidationErrors:
helix.WriteValidationProblem(w, e)
default:
// Always handle unknown errors
logs.ErrorContext(r.Context(), "unhandled error", logs.Err(err))
helix.WriteProblem(w, helix.ErrInternal.WithDetail("an error occurred"))
}
}import "github.com/kolosys/helix/logs"
s.GET("/users/{id}", helix.HandleCtx(func(c *helix.Ctx) error {
user, err := userService.Get(c.Context(), c.Param("id"))
if err != nil {
logs.ErrorContext(c.Context(), "failed to get user",
logs.Err(err),
logs.String("user_id", c.Param("id")),
)
return helix.NotFoundf("user not found")
}
return c.OK(user)
}))func errorTrackingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rw := &trackingResponseWriter{ResponseWriter: w}
next.ServeHTTP(rw, r)
if rw.status >= 400 {
metrics.IncrementErrorCounter(r.Method, r.URL.Path, rw.status)
}
})
}func apiErrorHandler(w http.ResponseWriter, r *http.Request, err error) {
var problem helix.Problem
switch e := err.(type) {
case helix.Problem:
problem = e
case *helix.ValidationErrors:
helix.WriteValidationProblem(w, e)
return
default:
// Log unexpected errors
logs.ErrorContext(r.Context(), "unexpected error", logs.Err(err))
problem = helix.ErrInternal.WithDetail("an error occurred")
}
// Add request ID to problem
if requestID := r.Header.Get("X-Request-ID"); requestID != "" {
problem = problem.WithDetailf("%s (request-id: %s)", problem.Detail, requestID)
}
helix.WriteProblem(w, problem)
}
s := helix.New(helix.WithErrorHandler(apiErrorHandler))