Skip to main content

Error Codes Reference

Contract uses a standard set of error codes that work across all protocols. This page explains each code, when to use it, and how it maps to HTTP, JSON-RPC, and other protocols.

Why Standard Error Codes?

Different protocols handle errors differently:
  • HTTP uses status codes (404, 500, etc.)
  • JSON-RPC uses negative error codes (-32600, -32601, etc.)
  • gRPC uses its own codes (NOT_FOUND, INTERNAL, etc.)
Contract uses protocol-agnostic codes that automatically translate to the right format:
// You write this once:
return nil, contract.ErrNotFound("user not found")

// And it becomes:
// - HTTP: 404 Not Found
// - JSON-RPC: {"error": {"code": -32601, "message": "user not found"}}
// - MCP: {"isError": true, "content": [{"type": "text", "text": "user not found"}]}

Quick Reference Table

Error CodeHTTPJSON-RPCUse When
INVALID_ARGUMENT400-32602Bad input from user
NOT_FOUND404-32601Resource doesn’t exist
ALREADY_EXISTS409-32003Trying to create duplicate
PERMISSION_DENIED403-32004User can’t do this
UNAUTHENTICATED401-32011User not logged in
RESOURCE_EXHAUSTED429-32005Rate limited
FAILED_PRECONDITION412-32006System not ready
INTERNAL500-32603Server bug
UNAVAILABLE503-32009Service down
UNIMPLEMENTED501-32601Feature not built

Detailed Error Code Guide

INVALID_ARGUMENT

Use when: The user sent bad input.
func (s *Service) Create(ctx context.Context, in *CreateInput) (*Todo, error) {
    if in.Title == "" {
        return nil, contract.ErrInvalidArgument("title is required")
    }
    if len(in.Title) > 500 {
        return nil, contract.ErrInvalidArgument("title must be 500 characters or less")
    }
    if !isValidEmail(in.Email) {
        return nil, contract.ErrInvalidArgument("email format is invalid")
    }
    // ...
}
Common scenarios:
  • Missing required field
  • Value too long or too short
  • Invalid format (email, URL, phone)
  • Number out of allowed range
  • Wrong data type
HTTP Status: 400 Bad Request

NOT_FOUND

Use when: The requested item doesn’t exist.
func (s *Service) Get(ctx context.Context, in *GetInput) (*Todo, error) {
    todo, ok := s.todos[in.ID]
    if !ok {
        return nil, contract.ErrNotFound("todo not found")
    }
    return todo, nil
}
Common scenarios:
  • Getting item by ID that doesn’t exist
  • Referencing deleted resource
  • Looking up user by email that’s not registered
With extra details:
return nil, contract.ErrNotFound("todo not found").WithDetail("id", in.ID)
HTTP Status: 404 Not Found

ALREADY_EXISTS

Use when: Trying to create something that already exists.
func (s *Service) Create(ctx context.Context, in *CreateInput) (*User, error) {
    if s.emailExists(in.Email) {
        return nil, contract.ErrAlreadyExists("email already registered")
    }
    // ...
}
Common scenarios:
  • Creating user with existing email
  • Creating resource with duplicate unique ID
  • Adding item that’s already in a list
HTTP Status: 409 Conflict

PERMISSION_DENIED

Use when: User is logged in but not allowed to do this.
func (s *Service) Delete(ctx context.Context, in *DeleteInput) error {
    user := UserFromContext(ctx)
    todo := s.todos[in.ID]

    // Only owner or admin can delete
    if todo.OwnerID != user.ID && user.Role != "admin" {
        return contract.ErrPermissionDenied("you can only delete your own todos")
    }
    // ...
}
Common scenarios:
  • User trying to access another user’s data
  • User without admin role trying admin action
  • User trying to modify read-only resource
HTTP Status: 403 Forbidden Note: Use UNAUTHENTICATED if user isn’t logged in at all.

UNAUTHENTICATED

Use when: User needs to log in first.
func (s *Service) Create(ctx context.Context, in *CreateInput) (*Todo, error) {
    user := UserFromContext(ctx)
    if user == nil {
        return nil, contract.ErrUnauthenticated("please log in first")
    }
    // ...
}
Common scenarios:
  • No authentication token provided
  • Token has expired
  • Token is invalid or malformed
HTTP Status: 401 Unauthorized vs PERMISSION_DENIED:
  • UNAUTHENTICATED: “Who are you?” (not logged in)
  • PERMISSION_DENIED: “I know who you are, but you can’t do this” (logged in but not allowed)

RESOURCE_EXHAUSTED

Use when: Some limit has been reached.
func (s *Service) Create(ctx context.Context, in *CreateInput) (*Todo, error) {
    if s.rateLimiter.IsExceeded(ctx) {
        return nil, contract.ErrResourceExhausted("too many requests, try again later")
    }
    if s.storage.IsFull() {
        return nil, contract.ErrResourceExhausted("storage quota exceeded")
    }
    // ...
}
Common scenarios:
  • Rate limit exceeded
  • Storage quota full
  • Too many items created
  • Concurrent request limit
HTTP Status: 429 Too Many Requests

FAILED_PRECONDITION

Use when: System isn’t in the right state for this operation.
func (s *Service) Publish(ctx context.Context, in *PublishInput) error {
    article := s.articles[in.ID]

    if article.Status != "draft" {
        return contract.ErrFailedPrecondition("can only publish draft articles")
    }
    if !article.HasContent {
        return contract.ErrFailedPrecondition("article has no content")
    }
    // ...
}
Common scenarios:
  • Resource in wrong state for operation
  • Required setup not complete
  • Dependency not satisfied
  • Order of operations violated
HTTP Status: 412 Precondition Failed

INTERNAL

Use when: Something unexpected went wrong on the server.
func (s *Service) Create(ctx context.Context, in *CreateInput) (*Todo, error) {
    err := s.db.Save(todo)
    if err != nil {
        // Log the real error for debugging
        log.Printf("database error: %v", err)

        // Return generic message to user (don't leak internals!)
        return nil, contract.ErrInternal("failed to save todo")
    }
    // ...
}
With cause for logging:
return nil, contract.ErrInternal("failed to save").WithCause(dbErr)
// The cause is logged but NOT exposed to the client
Common scenarios:
  • Database errors
  • Unexpected nil values
  • Bug in your code
  • External service returned unexpected response
HTTP Status: 500 Internal Server Error Important: Never expose internal details to clients:
// Bad: Leaks internal info
contract.ErrInternal("postgres: connection refused to 10.0.0.1:5432")

// Good: Safe message
contract.ErrInternal("database temporarily unavailable")

UNAVAILABLE

Use when: Service is temporarily unavailable.
func (s *Service) Create(ctx context.Context, in *CreateInput) (*Todo, error) {
    if s.isInMaintenanceMode() {
        return nil, contract.ErrUnavailable("service is under maintenance")
    }
    if !s.db.IsConnected() {
        return nil, contract.ErrUnavailable("service temporarily unavailable")
    }
    // ...
}
Common scenarios:
  • Planned maintenance
  • Database connection lost
  • Dependent service is down
  • Server overloaded
HTTP Status: 503 Service Unavailable vs INTERNAL:
  • UNAVAILABLE: Temporary problem, try again later
  • INTERNAL: Something is broken, might need fixing

UNIMPLEMENTED

Use when: Feature doesn’t exist yet.
func (s *Service) Export(ctx context.Context, in *ExportInput) (*File, error) {
    if in.Format == "pdf" {
        return nil, contract.ErrUnimplemented("PDF export coming soon")
    }
    // ...
}
Common scenarios:
  • Feature not built yet
  • Method stub that’s not implemented
  • Format or option not supported
HTTP Status: 501 Not Implemented

Other Error Codes

These are less commonly used but available:
CodeHTTPUse When
CANCELED499Request was canceled by client
UNKNOWN500Error doesn’t fit other categories
DEADLINE_EXCEEDED504Operation timed out
ABORTED409Concurrent modification conflict
OUT_OF_RANGE400Pagination or index out of bounds
DATA_LOSS500Unrecoverable data corruption

Creating Errors

Convenience Functions

The most common errors have shortcut functions:
contract.ErrNotFound("user not found")
contract.ErrInvalidArgument("title required")
contract.ErrPermissionDenied("admin only")
contract.ErrUnauthenticated("please log in")
contract.ErrInternal("something went wrong")
contract.ErrAlreadyExists("email taken")
contract.ErrResourceExhausted("rate limit exceeded")
contract.ErrUnimplemented("coming soon")
contract.ErrUnavailable("maintenance mode")

General Constructor

For other codes, use NewError:
contract.NewError(contract.ErrCodeCanceled, "request was canceled")
contract.NewError(contract.ErrCodeDeadlineExceeded, "operation timed out")
contract.NewError(contract.ErrCodeAborted, "concurrent modification")

With Format String

contract.Errorf(contract.ErrCodeNotFound, "user %s not found", userID)

Adding Context to Errors

Add Details

Include structured data for debugging:
err := contract.ErrNotFound("todo not found").
    WithDetail("id", todoID).
    WithDetail("searched_at", time.Now())

Add Multiple Details

err := contract.ErrInvalidArgument("validation failed").
    WithDetails(map[string]any{
        "field": "email",
        "value": email,
        "reason": "invalid format",
    })

Add Cause

Wrap the underlying error (useful for logging):
result, err := s.db.Query(...)
if err != nil {
    return nil, contract.ErrInternal("database query failed").WithCause(err)
    // The cause is available for logging but NOT sent to the client
}

Checking Error Types

In Your Code

import "errors"

_, err := svc.Get(ctx, &GetInput{ID: "123"})
if err != nil {
    var contractErr *contract.Error
    if errors.As(err, &contractErr) {
        switch contractErr.Code {
        case contract.ErrCodeNotFound:
            // Handle not found
        case contract.ErrCodePermissionDenied:
            // Handle forbidden
        default:
            // Handle other errors
        }
    }
}

Get HTTP Status

err := contract.ErrNotFound("not found")
status := err.HTTPStatus()  // Returns 404

Convert HTTP Status to Error Code

code := contract.HTTPStatusToErrorCode(404)  // Returns ErrCodeNotFound

How Errors Look in Different Protocols

REST

HTTP/1.1 404 Not Found
Content-Type: text/plain

user not found

JSON-RPC

{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": -32601,
    "message": "user not found"
  }
}

MCP

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "content": [
      {"type": "text", "text": "user not found"}
    ],
    "isError": true
  }
}

Decision Guide

Use this flowchart to pick the right error code:
Is the user logged in?
├─ No → UNAUTHENTICATED
└─ Yes (or not required)

   Is the input valid?
   ├─ No → INVALID_ARGUMENT
   └─ Yes

      Does the resource exist?
      ├─ No → NOT_FOUND
      └─ Yes

         Can this user access it?
         ├─ No → PERMISSION_DENIED
         └─ Yes

            Is the system ready?
            ├─ No, maintenance → UNAVAILABLE
            ├─ No, rate limit → RESOURCE_EXHAUSTED
            ├─ No, wrong state → FAILED_PRECONDITION
            └─ Yes

               Did something break?
               ├─ Yes → INTERNAL
               └─ No → (success!)

Best Practices

1. Be Specific But Safe

// Good: Specific but safe
contract.ErrNotFound("user not found")

// Bad: Leaks information
contract.ErrNotFound("user with email [email protected] not found")

2. Use the Right Code

// Good: UNAUTHENTICATED for "not logged in"
if user == nil {
    return nil, contract.ErrUnauthenticated("please log in")
}

// Bad: Using INTERNAL for auth failures
if user == nil {
    return nil, contract.ErrInternal("no user")  // Wrong code!
}

3. Log Internal Errors

if err := s.db.Save(item); err != nil {
    // Log the real error
    log.Printf("database error: %v", err)

    // Return safe error to user
    return nil, contract.ErrInternal("failed to save")
}

4. Add Context When Helpful

// Helpful for debugging
contract.ErrNotFound("todo not found").WithDetail("id", todoID)

// Not helpful (obvious from context)
contract.ErrNotFound("not found").WithDetail("type", "todo")

See Also