Putnami
DocsGitHub

Licensed under FSL-1.1-MIT

Getting Started
Concepts
How To
Build A Web App
Build An Api Service
Share Code Between Projects
Configure Your App
Add Persistence
Add Authentication
Add Background Jobs
Principles
Tooling & Workspace
Workspace Overview
Cli
Jobs & Commands
SDK
Error Handling
Extensions
Typescript
Go
Python
Docker
Ci
Frameworks
Typescript
OverviewWebReact RoutingForms And ActionsStatic FilesApiErrors And ResponsesConfigurationLoggingHttp And MiddlewareDependency InjectionPlugins And LifecycleSessionsAuthPersistenceEventsStorageCachingWebsocketsTestingHealth ChecksTelemetryProto GrpcSmart Client
Go
OverviewHttpDependency InjectionPlugins And LifecycleConfigurationSecurityPersistenceErrorsEventsStorageCachingLoggingTelemetryGrpcService ClientsValidationOpenapiTesting
Platform
  1. DocsSeparator
  2. FrameworksSeparator
  3. GoSeparator
  4. Errors

Errors

go.putnami.dev/errors provides a structured error model with typed codes, categories, attributes, stack traces, and HTTP mapping. It implements the standard error interface and integrates with observability systems.

Creating errors

Basic errors

import "go.putnami.dev/errors"

// New error with code and message (captures stack trace)
err := errors.New("user.not_found", "user not found")

// Formatted message
err := errors.Newf("user.not_found", "user %s not found", userID)

Wrapping errors

// Wrap an existing error with a code
result, err := db.Query(ctx, query)
if err != nil {
    return errors.Wrap(err, "db.query")
}

// Wrap with a custom message
return errors.Wrapf(err, "db.query", "failed to fetch user %s", userID)

Convenience constructors

// User-facing error (no stack trace)
err := errors.User("validation.email", "invalid email address")

// Bug — invariant violation (captures stack trace, category=bug)
err := errors.Bug(err)

// Common HTTP errors
err := errors.BadRequest("invalid input")     // 400
err := errors.Unauthorized("not authenticated") // 401
err := errors.Forbidden("access denied")       // 403
err := errors.NotFound("user not found")        // 404

Error codes

Codes use a dotted namespace convention:

type Code string

// Framework codes
const (
    CodeInternal     Code = "internal"
    CodeNotFound     Code = "not_found"
    CodeValidation   Code = "validation"
    CodeTimeout      Code = "timeout"
    CodeUnauthorized Code = "unauthorized"
    CodeForbidden    Code = "forbidden"
    CodeConflict     Code = "conflict"
    CodeBadRequest   Code = "bad_request"
    CodeUnavailable  Code = "unavailable"
    CodeCancelled    Code = "cancelled"
    CodeRateLimited  Code = "rate_limited"
)

Define domain-specific codes:

const (
    CodeUserNotFound  errors.Code = "user.not_found"
    CodeEmailTaken    errors.Code = "user.email_taken"
    CodePaymentFailed errors.Code = "payment.failed"
)

Categories

Categories classify errors for operational handling:

type Category string

const (
    CategoryInfra    Category = "infra"     // Infrastructure failures (DB, network)
    CategoryUser     Category = "user"      // User input errors
    CategoryTransient Category = "transient" // Retryable failures
    CategoryBug      Category = "bug"       // Invariant violations
    CategorySecurity Category = "security"  // Auth/authz failures
)

Set a category:

err := errors.New("db.connection", "connection refused").
    WithCategory(errors.CategoryInfra)

Attributes

Attach structured metadata to errors:

err := errors.New("order.failed", "order processing failed",
    errors.String("orderId", orderID),
    errors.Int("amount", 4999),
    errors.Float("retryAfter", 2.5),
    errors.Bool("retriable", true),
    errors.Error("cause", originalErr),
)
Function Type Description
errors.String(key, val) string String attribute
errors.Int(key, val) int Integer attribute
errors.Int64(key, val) int64 64-bit integer attribute
errors.Float(key, val) float64 Float attribute
errors.Bool(key, val) bool Boolean attribute
errors.Error(key, err) error Error attribute

Inspecting errors

Error methods

var err *errors.Error

err.Code()       // errors.Code
err.Message()    // string
err.Category()   // errors.Category
err.Cause()      // error (wrapped error)
err.Stack()      // string (call stack, if captured)
err.Attrs()      // []errors.Attr
err.IsRetryable() // bool
err.Error()      // string (implements error interface)

Checking error codes

if errors.Is(err, "user.not_found") {
    // handle not found
}

// Extract the structured error
if e := errors.GetError(err); e != nil {
    fmt.Println(e.Code(), e.Message())
}

// Get the code (returns CodeUnknown for non-structured errors)
code := errors.GetCode(err)

// Check retryability
if errors.IsRetryable(err) {
    // retry the operation
}

HTTP mapping

Errors automatically map to HTTP status codes:

// In an HTTP handler
func handler(ctx *fhttp.Context) *fhttp.Response {
    user, err := userService.Find(ctx.Context(), ctx.Param("id"))
    if err != nil {
        // WriteHTTPError maps error code → HTTP status
        errors.WriteHTTPError(ctx.Writer, err)
        return nil
    }
    return fhttp.JSON(user)
}

Code → HTTP status mapping

Error Code HTTP Status
bad_request 400
unauthorized 401
forbidden 403
not_found 404
conflict 409
validation 422
rate_limited 429
internal 500
unavailable 503
timeout 504

Stack traces

Stack traces are captured by New, Newf, and Bug. They are omitted by User and Wrap to avoid noise for expected errors:

// Stack trace captured
err := errors.New("bug.nil_pointer", "unexpected nil")
fmt.Println(err.Stack())
// go.putnami.dev/myservice.ProcessOrder
//     /app/service.go:42
// go.putnami.dev/myservice.Handler
//     /app/handler.go:15

// No stack trace (user-facing error)
err := errors.User("validation.email", "invalid email")
fmt.Println(err.Stack()) // ""

Framework error codes

Each framework package defines its own error codes:

Package Codes
inject inject.not_registered, inject.circular_dependency, inject.scope_violation, inject.container_closed, inject.duplicate_provider, inject.requirement_not_met, inject.type_mismatch, inject.factory_failed
app app.already_running, app.warmup, app.register, app.start, app.stop, app.shutdown, app.invoke, app.runner
sql db.connection, db.query, db.migration, db.transaction
http http.listen, http.body, http.scope
config config.source, config.path, config.mapping

Patterns

Service layer errors

func (s *UserService) Create(ctx context.Context, input CreateUserInput) (*User, error) {
    existing, err := s.repo.FindByEmail(ctx, input.Email)
    if err != nil && !errors.Is(err, "not_found") {
        return nil, errors.Wrap(err, "db.query")
    }
    if existing != nil {
        return nil, errors.User("user.email_taken", "email already registered",
            errors.String("email", input.Email),
        )
    }

    user, err := s.repo.Create(ctx, input)
    if err != nil {
        return nil, errors.Wrapf(err, "user.create", "failed to create user %s", input.Email)
    }
    return user, nil
}

Error handling in handlers

func getUser(ctx *fhttp.Context) *fhttp.Response {
    user, err := userService.Find(ctx.Context(), ctx.Param("id"))
    if err != nil {
        if errors.Is(err, "not_found") {
            return fhttp.NotFound()
        }
        return fhttp.InternalError(err.Error())
    }
    return fhttp.JSON(user)
}

Related guides

  • HTTP & Middleware — error responses
  • Telemetry — error metrics
  • Logging — error logging

On this page

  • Errors
  • Creating errors
  • Basic errors
  • Wrapping errors
  • Convenience constructors
  • Error codes
  • Categories
  • Attributes
  • Inspecting errors
  • Error methods
  • Checking error codes
  • HTTP mapping
  • Code → HTTP status mapping
  • Stack traces
  • Framework error codes
  • Patterns
  • Service layer errors
  • Error handling in handlers
  • Related guides