Skip to main content
Kratos provides a rich error system that works seamlessly across HTTP and gRPC transports with support for error codes, reasons, metadata, and cause chains.

Error Structure

errors/errors.go:23-26
type Error struct {
    Status
    cause error
}
The Error type includes:
  • Code: HTTP/gRPC status code
  • Reason: Application-specific error reason
  • Message: Human-readable error message
  • Metadata: Additional error context
  • Cause: Underlying error (for error chains)

Creating Errors

Basic Error

errors/errors.go:68-76
import "github.com/go-kratos/kratos/v2/errors"

err := errors.New(404, "USER_NOT_FOUND", "user not found")
// Code: 404
// Reason: USER_NOT_FOUND
// Message: user not found

Formatted Error

errors/errors.go:78-81
err := errors.Newf(500, "DATABASE_ERROR", "failed to query user: %s", username)

With Metadata

errors/errors.go:51-55
err := errors.New(400, "VALIDATION_ERROR", "invalid input")
err = err.WithMetadata(map[string]string{
    "field": "email",
    "value": "invalid-email",
})

With Cause

errors/errors.go:44-48
originalErr := database.Query()
err := errors.New(500, "DATABASE_ERROR", "query failed")
err = err.WithCause(originalErr)

// Access underlying error
cause := errors.Unwrap(err) // Returns originalErr

Predefined Errors

Kratos provides constructors for common HTTP status codes:
// 400 Bad Request
errors.BadRequest("INVALID_PARAM", "invalid parameter")

// 401 Unauthorized
errors.Unauthorized("TOKEN_EXPIRED", "authentication token expired")

// 403 Forbidden
errors.Forbidden("ACCESS_DENIED", "access denied")

// 404 Not Found
errors.NotFound("RESOURCE_NOT_FOUND", "resource not found")

// 409 Conflict
errors.Conflict("ALREADY_EXISTS", "resource already exists")

// 500 Internal Server Error
errors.InternalServer("INTERNAL_ERROR", "internal server error")

// 503 Service Unavailable
errors.ServiceUnavailable("SERVICE_DOWN", "service temporarily unavailable")

// 504 Gateway Timeout
errors.GatewayTimeout("TIMEOUT", "request timeout")

Error Inspection

Get Error Code

errors/errors.go:90-95
err := someOperation()
code := errors.Code(err)
if code == 404 {
    // Handle not found
}

Get Error Reason

errors/errors.go:98-104
err := someOperation()
reason := errors.Reason(err)
if reason == "USER_NOT_FOUND" {
    // Handle specific error
}

Convert to Kratos Error

errors/errors.go:128-152
err := someOperation()
if err != nil {
    kratosErr := errors.FromError(err)
    log.Errorf("Code: %d, Reason: %s, Message: %s",
        kratosErr.Code, kratosErr.Reason, kratosErr.Message)
}

Error Matching

errors/errors.go:36-41
targetErr := errors.NotFound("USER_NOT_FOUND", "")
err := someOperation()

if errors.Is(err, targetErr) {
    // Error matches code and reason
}

HTTP Integration

Errors are automatically converted to appropriate HTTP responses:
import (
    "github.com/go-kratos/kratos/v2/errors"
    "github.com/go-kratos/kratos/v2/transport/http"
)

func HandleGetUser(ctx http.Context) error {
    user, err := userRepo.Get(ctx, userID)
    if err != nil {
        // Return Kratos error
        return errors.NotFound("USER_NOT_FOUND", "user not found")
    }
    return ctx.Result(200, user)
}

// HTTP Response:
// Status: 404
// Body: {
//   "code": 404,
//   "reason": "USER_NOT_FOUND",
//   "message": "user not found"
// }

Custom Error Encoder

func customErrorEncoder(w http.ResponseWriter, r *http.Request, err error) {
    se := errors.FromError(err)
    
    // Custom error response format
    response := map[string]interface{}{
        "error": map[string]interface{}{
            "code":    se.Code,
            "reason":  se.Reason,
            "message": se.Message,
            "details": se.Metadata,
        },
    }
    
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(int(se.Code))
    json.NewEncoder(w).Encode(response)
}

httpSrv := http.NewServer(
    http.ErrorEncoder(customErrorEncoder),
)

gRPC Integration

Errors are automatically converted to gRPC status:
errors/errors.go:58-65
import (
    "github.com/go-kratos/kratos/v2/errors"
)

func (s *GreeterService) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloReply, error) {
    if req.Name == "" {
        return nil, errors.BadRequest("EMPTY_NAME", "name is required")
    }
    return &pb.HelloReply{Message: "Hello " + req.Name}, nil
}

// gRPC Client receives:
// Code: InvalidArgument
// Message: name is required
// Details: ErrorInfo with Reason=EMPTY_NAME

GRPCStatus

Convert to native gRPC status:
err := errors.New(404, "NOT_FOUND", "resource not found")
grpcStatus := err.GRPCStatus()
// Returns *status.Status with ErrorInfo details

Error Chains

Wrapping Errors

import "github.com/go-kratos/kratos/v2/errors"

func GetUser(id int) (*User, error) {
    user, err := db.Query(id)
    if err != nil {
        return nil, errors.InternalServer("DATABASE_ERROR", "failed to query user").WithCause(err)
    }
    return user, nil
}

func HandleRequest(ctx context.Context) error {
    user, err := GetUser(123)
    if err != nil {
        // Original database error is preserved in cause
        return errors.InternalServer("REQUEST_FAILED", "request processing failed").WithCause(err)
    }
    return nil
}

Unwrapping Errors

err := someOperation()

// Get underlying error
if se := errors.FromError(err); se != nil {
    cause := errors.Unwrap(se)
    log.Errorf("Caused by: %v", cause)
}

Error Metadata

Attach additional context to errors:
err := errors.BadRequest("VALIDATION_ERROR", "validation failed")
err = err.WithMetadata(map[string]string{
    "field":     "email",
    "value":     "invalid@",
    "constraint": "must be valid email",
})

// Access metadata
se := errors.FromError(err)
for key, value := range se.Metadata {
    log.Infof("%s: %s", key, value)
}

Best Practices

Use UPPER_SNAKE_CASE reasons that clearly identify the error type (e.g., USER_NOT_FOUND, INVALID_TOKEN).
Provide helpful error messages that aid in debugging without exposing sensitive information.
Use WithCause() to preserve underlying errors for debugging while returning clean errors to clients.
Convert internal errors (like database errors) to appropriate public errors before returning to clients.
Add validation errors and field-specific information to metadata rather than cramming it into the message.
Define error reasons as constants and use them consistently across your application.

Error Constants Example

package errs

import "github.com/go-kratos/kratos/v2/errors"

// Define error reasons as constants
const (
    ReasonUserNotFound     = "USER_NOT_FOUND"
    ReasonInvalidEmail     = "INVALID_EMAIL"
    ReasonEmailExists      = "EMAIL_EXISTS"
    ReasonUnauthorized     = "UNAUTHORIZED"
    ReasonTokenExpired     = "TOKEN_EXPIRED"
    ReasonInternalError    = "INTERNAL_ERROR"
)

// Error constructors
func ErrorUserNotFound(msg string) *errors.Error {
    return errors.NotFound(ReasonUserNotFound, msg)
}

func ErrorInvalidEmail(email string) *errors.Error {
    return errors.BadRequest(ReasonInvalidEmail, "invalid email").WithMetadata(map[string]string{
        "email": email,
    })
}

func ErrorEmailExists(email string) *errors.Error {
    return errors.Conflict(ReasonEmailExists, "email already exists").WithMetadata(map[string]string{
        "email": email,
    })
}

// Usage
func CreateUser(email string) error {
    if !isValidEmail(email) {
        return errs.ErrorInvalidEmail(email)
    }
    
    exists, err := checkEmailExists(email)
    if err != nil {
        return errors.InternalServer(errs.ReasonInternalError, "failed to check email").WithCause(err)
    }
    if exists {
        return errs.ErrorEmailExists(email)
    }
    
    return nil
}

HTTP Transport

HTTP error encoding

gRPC Transport

gRPC error handling

Middleware

Error recovery middleware

Logging

Error logging

Build docs developers (and LLMs) love