Documentation
¶
Overview ¶
Package errors provides framework-agnostic error formatting for HTTP responses.
The package defines a Formatter interface and provides concrete implementations for different error response formats:
- RFC9457: RFC 9457 Problem Details (application/problem+json)
- JSONAPI: JSON:API error responses (application/vnd.api+json)
- Simple: Simple JSON error responses (application/json)
The package is independent of any HTTP framework and can be used with any HTTP handler. Domain errors can implement optional interfaces (ErrorType, ErrorDetails, ErrorCode) to control status codes and provide structured details.
Quick Start ¶
Basic usage with RFC 9457 format:
package main
import (
"encoding/json"
"net/http"
"rivaas.dev/errors"
)
func handler(w http.ResponseWriter, r *http.Request) {
err := someOperation()
if err != nil {
formatter := errors.NewRFC9457("https://api.example.com/problems")
response := formatter.Format(r, err)
w.Header().Set("Content-Type", response.ContentType)
w.WriteHeader(response.Status)
json.NewEncoder(w).Encode(response.Body)
return
}
}
JSON:API format:
formatter := errors.NewJSONAPI()
response := formatter.Format(r, err)
w.Header().Set("Content-Type", response.ContentType)
w.WriteHeader(response.Status)
json.NewEncoder(w).Encode(response.Body)
Simple JSON format:
formatter := errors.NewSimple()
response := formatter.Format(r, err)
w.Header().Set("Content-Type", response.ContentType)
w.WriteHeader(response.Status)
json.NewEncoder(w).Encode(response.Body)
Error Interfaces ¶
Domain errors can implement optional interfaces to provide additional information:
- ErrorType: Declare HTTP status code
- ErrorDetails: Provide structured details (e.g., field-level validation errors)
- ErrorCode: Provide machine-readable error codes
Example error with all interfaces:
type ValidationError struct {
Message string
Fields []FieldError
Code string
}
func (e ValidationError) Error() string {
return e.Message
}
func (e ValidationError) HTTPStatus() int {
return http.StatusBadRequest
}
func (e ValidationError) Details() any {
return e.Fields
}
func (e ValidationError) Code() string {
return e.Code
}
Examples ¶
See the example_test.go file for complete working examples.
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type ErrorCode ¶
ErrorCode allows errors to provide a machine-readable code. Domain errors can implement this interface to expose application-specific error codes.
Example:
type NotFoundError struct {
Resource string
}
func (e NotFoundError) Error() string {
return fmt.Sprintf("%s not found", e.Resource)
}
func (e NotFoundError) Code() string {
return "RESOURCE_NOT_FOUND"
}
type ErrorDetails ¶
type ErrorDetails interface {
error
// Details returns structured information about the error.
Details() any
}
ErrorDetails allows errors to provide additional structured information. Domain errors can implement this interface to expose field-level details.
Example:
type ValidationError struct {
Message string
Fields []FieldError
}
func (e ValidationError) Error() string {
return e.Message
}
func (e ValidationError) Details() any {
return e.Fields
}
type ErrorType ¶
type ErrorType interface {
error
// HTTPStatus returns the HTTP status code for this error.
HTTPStatus() int
}
ErrorType allows errors to declare their own HTTP status code. Domain errors can optionally implement this interface to control their status code.
Example:
type ValidationError struct {
Message string
}
func (e ValidationError) Error() string {
return e.Message
}
func (e ValidationError) HTTPStatus() int {
return http.StatusBadRequest
}
type Formatter ¶
type Formatter interface {
// Format converts an error into HTTP response components.
// It returns status code, content-type, and response body.
//
// Example:
//
// response := formatter.Format(req, err)
// w.Header().Set("Content-Type", response.ContentType)
// w.WriteHeader(response.Status)
// json.NewEncoder(w).Encode(response.Body)
//
// Parameters:
// - req: HTTP request context (used for instance URI in RFC9457)
// - err: Error to format
//
// Returns a Response containing status code, content type, and body.
Format(req *http.Request, err error) Response
}
Formatter defines how errors are formatted in HTTP responses. Implementations are framework-agnostic and work with any HTTP handler.
Example:
formatter := errors.NewRFC9457("https://api.example.com/problems")
response := formatter.Format(req, err)
w.Header().Set("Content-Type", response.ContentType)
w.WriteHeader(response.Status)
json.NewEncoder(w).Encode(response.Body)
type JSONAPI ¶
type JSONAPI struct {
// StatusResolver determines HTTP status from error.
// If nil, uses ErrorType interface or defaults to 500.
StatusResolver func(err error) int
}
JSONAPI formats errors per JSON:API specification. It produces responses with Content-Type "application/vnd.api+json". See: https://jsonapi.org/format/#errors
Example:
formatter := errors.NewJSONAPI()
response := formatter.Format(req, err)
w.Header().Set("Content-Type", response.ContentType)
w.WriteHeader(response.Status)
json.NewEncoder(w).Encode(response.Body)
Example ¶
ExampleJSONAPI demonstrates how to use the JSONAPI formatter.
package main
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"rivaas.dev/errors"
stderrors "errors"
)
func main() {
// Create a formatter
formatter := errors.NewJSONAPI()
// Create a test error
err := stderrors.New("resource not found")
// Create a request
req := httptest.NewRequest(http.MethodGet, "/api/users/123", nil)
// Format the error
response := formatter.Format(req, err)
// Write the response
w := httptest.NewRecorder()
w.WriteHeader(response.Status)
w.Header().Set("Content-Type", response.ContentType)
_ = json.NewEncoder(w).Encode(response.Body)
_, _ = fmt.Printf("Status: %d\n", response.Status)
_, _ = fmt.Printf("Content-Type: %s\n", response.ContentType)
}
Output: Status: 500 Content-Type: application/vnd.api+json; charset=utf-8
func NewJSONAPI ¶
func NewJSONAPI() *JSONAPI
NewJSONAPI creates a new JSONAPI formatter.
Example:
formatter := errors.NewJSONAPI()
response := formatter.Format(req, err)
w.Header().Set("Content-Type", response.ContentType)
w.WriteHeader(response.Status)
json.NewEncoder(w).Encode(response.Body)
Returns a new JSONAPI formatter instance.
func (*JSONAPI) Format ¶
Format converts an error into a JSON:API error response. If the error implements ErrorDetails, it converts field-level errors into multiple JSON:API error objects. If the error implements ErrorCode, it includes the code in the error object.
Example:
formatter := errors.NewJSONAPI()
response := formatter.Format(req, err)
w.Header().Set("Content-Type", response.ContentType)
w.WriteHeader(response.Status)
json.NewEncoder(w).Encode(response.Body)
Parameters:
- req: HTTP request (currently unused, reserved for future use)
- err: Error to format
Returns a Response with JSON:API formatted error.
type ProblemDetail ¶
type ProblemDetail struct {
Type string `json:"type"`
Title string `json:"title"`
Status int `json:"status"`
Detail string `json:"detail,omitempty"`
Instance string `json:"instance,omitempty"`
Extensions map[string]any `json:"-"` // Marshaled inline
}
ProblemDetail represents an RFC 9457 problem detail. It contains the standard problem detail fields plus extensions.
Example:
p := ProblemDetail{
Type: "https://api.example.com/problems/validation-error",
Title: "Validation Error",
Status: 400,
Detail: "The request contains invalid data",
Instance: "/api/users",
Extensions: map[string]any{
"errors": []FieldError{...},
},
}
func (ProblemDetail) MarshalJSON ¶
func (p ProblemDetail) MarshalJSON() ([]byte, error)
MarshalJSON implements custom JSON marshaling to include extensions inline. It merges extension fields into the main JSON object while protecting reserved field names.
Returns the JSON-encoded problem detail with extensions merged inline.
type RFC9457 ¶
type RFC9457 struct {
// BaseURL is prepended to problem type slugs to create full URIs.
// Example: "https://api.example.com/problems" + "/validation-error"
BaseURL string
// TypeResolver maps error types/codes to problem type URIs.
// If nil, uses default mapping based on ErrorCode interface.
TypeResolver func(err error) string
// StatusResolver determines HTTP status from error.
// If nil, uses default logic (ErrorType interface, then 500).
StatusResolver func(err error) int
// ErrorIDGenerator generates unique IDs for error tracking.
// If nil, uses default UUID-based generation.
ErrorIDGenerator func() string
// DisableErrorID disables automatic error ID generation.
DisableErrorID bool
}
RFC9457 formats errors as RFC 9457 Problem Details. It produces responses with Content-Type "application/problem+json".
Example:
formatter := errors.NewRFC9457("https://api.example.com/problems")
response := formatter.Format(req, err)
w.Header().Set("Content-Type", response.ContentType)
w.WriteHeader(response.Status)
json.NewEncoder(w).Encode(response.Body)
Example ¶
ExampleRFC9457 demonstrates how to use the RFC9457 formatter.
package main
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"rivaas.dev/errors"
stderrors "errors"
)
func main() {
// Create a formatter with a base URL for problem types
formatter := errors.NewRFC9457("https://api.example.com/problems")
// Create a test error
err := stderrors.New("validation failed")
// Create a request
req := httptest.NewRequest(http.MethodPost, "/api/users", nil)
// Format the error
response := formatter.Format(req, err)
// Write the response
w := httptest.NewRecorder()
w.WriteHeader(response.Status)
w.Header().Set("Content-Type", response.ContentType)
_ = json.NewEncoder(w).Encode(response.Body)
_, _ = fmt.Printf("Status: %d\n", response.Status)
_, _ = fmt.Printf("Content-Type: %s\n", response.ContentType)
}
Output: Status: 500 Content-Type: application/problem+json; charset=utf-8
Example (CustomErrorID) ¶
ExampleRFC9457_customErrorID demonstrates custom error ID generation.
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"rivaas.dev/errors"
stderrors "errors"
)
func main() {
// Create a formatter with custom error ID generator
formatter := &errors.RFC9457{
BaseURL: "https://api.example.com/problems",
ErrorIDGenerator: func() string {
return "custom-id-12345"
},
}
err := stderrors.New("test error")
req := httptest.NewRequest(http.MethodGet, "/test", nil)
response := formatter.Format(req, err)
body := response.Body.(errors.ProblemDetail)
_, _ = fmt.Printf("Error ID: %v\n", body.Extensions["error_id"])
}
Output: Error ID: custom-id-12345
Example (DisableErrorID) ¶
ExampleRFC9457_disableErrorID demonstrates disabling error ID generation.
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"rivaas.dev/errors"
stderrors "errors"
)
func main() {
// Create a formatter with error ID disabled
formatter := &errors.RFC9457{
BaseURL: "https://api.example.com/problems",
DisableErrorID: true,
}
err := stderrors.New("test error")
req := httptest.NewRequest(http.MethodGet, "/test", nil)
response := formatter.Format(req, err)
body := response.Body.(errors.ProblemDetail)
if _, ok := body.Extensions["error_id"]; !ok {
_, _ = fmt.Println("Error ID is disabled")
}
}
Output: Error ID is disabled
func NewRFC9457 ¶
NewRFC9457 creates a new RFC9457 formatter. The baseURL parameter is prepended to problem type slugs to create full URIs.
Example:
formatter := errors.NewRFC9457("https://api.example.com/problems")
response := formatter.Format(req, err)
Parameters:
- baseURL: Base URL for problem type URIs (e.g., "https://api.example.com/problems")
Returns a new RFC9457 formatter instance.
func (*RFC9457) Format ¶
Format converts an error into an RFC 9457 Problem Details response. It determines the status code, problem type, and builds the problem detail structure. If the error implements ErrorDetails or ErrorCode interfaces, those are included as extensions.
Example:
formatter := errors.NewRFC9457("https://api.example.com/problems")
response := formatter.Format(req, err)
w.Header().Set("Content-Type", response.ContentType)
w.WriteHeader(response.Status)
json.NewEncoder(w).Encode(response.Body)
Parameters:
- req: HTTP request (used for instance URI)
- err: Error to format
Returns a Response with RFC 9457 formatted error.
type Response ¶
type Response struct {
// Status is the HTTP status code.
Status int
// ContentType is the Content-Type header value.
ContentType string
// Body is the response body (will be marshaled to JSON/XML/etc).
Body any
// Headers contains additional headers to set (optional).
Headers http.Header
}
Response represents a formatted error response. It contains all components needed to write an HTTP error response.
Example:
response := formatter.Format(req, err)
w.Header().Set("Content-Type", response.ContentType)
if response.Headers != nil {
for k, v := range response.Headers {
for _, val := range v {
w.Header().Add(k, val)
}
}
}
w.WriteHeader(response.Status)
json.NewEncoder(w).Encode(response.Body)
type Simple ¶
type Simple struct {
// StatusResolver determines HTTP status from error.
// If nil, uses ErrorType interface or defaults to 500.
StatusResolver func(err error) int
}
Simple formats errors as simple JSON objects. It produces responses with Content-Type "application/json". Format: {"error": "message", "details": {...}, "code": "..."}
Example:
formatter := errors.NewSimple()
response := formatter.Format(req, err)
w.Header().Set("Content-Type", response.ContentType)
w.WriteHeader(response.Status)
json.NewEncoder(w).Encode(response.Body)
Example ¶
ExampleSimple demonstrates how to use the Simple formatter.
package main
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"rivaas.dev/errors"
stderrors "errors"
)
func main() {
// Create a formatter
formatter := errors.NewSimple()
// Create a test error
err := stderrors.New("internal server error")
// Create a request
req := httptest.NewRequest(http.MethodGet, "/api/health", nil)
// Format the error
response := formatter.Format(req, err)
// Write the response
w := httptest.NewRecorder()
w.WriteHeader(response.Status)
w.Header().Set("Content-Type", response.ContentType)
_ = json.NewEncoder(w).Encode(response.Body)
_, _ = fmt.Printf("Status: %d\n", response.Status)
_, _ = fmt.Printf("Content-Type: %s\n", response.ContentType)
}
Output: Status: 500 Content-Type: application/json; charset=utf-8
func NewSimple ¶
func NewSimple() *Simple
NewSimple creates a new Simple formatter.
Example:
formatter := errors.NewSimple()
response := formatter.Format(req, err)
w.Header().Set("Content-Type", response.ContentType)
w.WriteHeader(response.Status)
json.NewEncoder(w).Encode(response.Body)
Returns a new Simple formatter instance.
func (*Simple) Format ¶
Format converts an error into a simple JSON response. If the error implements ErrorDetails or ErrorCode interfaces, those are included in the response.
Example:
formatter := errors.NewSimple()
response := formatter.Format(req, err)
w.Header().Set("Content-Type", response.ContentType)
w.WriteHeader(response.Status)
json.NewEncoder(w).Encode(response.Body)
Parameters:
- req: HTTP request (currently unused, reserved for future use)
- err: Error to format
Returns a Response with simple JSON formatted error.