Skip to content

Error Handling

pkg/errors is a transport-agnostic error substrate. You return a single *AppError from a handler and it renders identically over HTTP (RFC 9457 application/problem+json) and gRPC (status + errdetails). The renderers live in pkg/errors/fiber and pkg/errors/grpc; import them aliased (e.g. errfiber, errgrpc) to avoid clashing with the fiber and grpc packages.

Construct an error with one of the code constructors, then enrich it with the chainable With* builders:

import errpkg "github.com/Vilsol/lakta/pkg/errors"
func (s *Server) GetUser(ctx context.Context, id string) (*User, error) {
u, err := s.repo.Find(ctx, id)
if err != nil {
return nil, errpkg.NotFound("user not found").
WithMeta("user_id", id).
WithCause(err)
}
return u, nil
}

Each constructor seeds a canonical Code and its fixed HTTP + gRPC statuses:

Constructor Code HTTP gRPC
NotFound NOT_FOUND 404 NotFound
InvalidArgument INVALID_ARGUMENT 400 InvalidArgument
Validation VALIDATION 400 InvalidArgument
Unauthenticated UNAUTHENTICATED 401 Unauthenticated
PermissionDenied PERMISSION_DENIED 403 PermissionDenied
AlreadyExists ALREADY_EXISTS 409 AlreadyExists
FailedPrecondition FAILED_PRECONDITION 400 FailedPrecondition
Unavailable UNAVAILABLE 503 Unavailable
Internal INTERNAL 500 Internal

Builders: WithField(field, desc) appends a field violation (rendered as invalid_params / errdetails.BadRequest), WithMeta(k, v) attaches metadata (rendered as errdetails.ErrorInfo.Metadata), and WithCause(err) wraps an origin error surfaced via Unwrap but never sent over the wire.

Any non-AppError returned from a handler is normalized by FromError: an existing *AppError passes through, an oops error maps its code (falling back to INTERNAL) and lifts its context into Meta, and anything else becomes an opaque INTERNAL.

Wire errfiber.ErrorHandler into the fiber module. Every handler error then renders as application/problem+json (never text/html):

import errfiber "github.com/Vilsol/lakta/pkg/errors/fiber"
fiberserver.NewModule(
fiberserver.WithErrorHandler(errfiber.ErrorHandler()),
)

The body carries type (urn:lakta:error:{code}), title, status, detail, code, and invalid_params. Raw framework errors (a *fiber.Error 404/405) are mapped to the equivalent AppError so they render identically. An INTERNAL error renders the opaque detail "internal error" — the message and cause are never leaked. The title field defaults to http.StatusText. ErrorHandler accepts variadic Option values for future customization.

Add the interceptors to the gRPC server module. They convert a returned error into a *status.Status with errdetails attached:

import errgrpc "github.com/Vilsol/lakta/pkg/errors/grpc"
grpcserver.NewModule(
grpcserver.WithUnaryInterceptor(errgrpc.UnaryServerInterceptor()),
grpcserver.WithStreamInterceptor(errgrpc.StreamServerInterceptor()),
grpcserver.WithService(&v1.MyService_ServiceDesc, NewServer()),
)

The status carries the mapped gRPC code and message, plus an errdetails.ErrorInfo (Reason = the string code, Metadata = Meta). When the error has field violations, an errdetails.BadRequest is attached as well. As with HTTP, an INTERNAL error renders the opaque message "internal error".