Documentation Index
Fetch the complete documentation index at: https://docs.go-mizu.dev/llms.txt
Use this file to discover all available pages before exploring further.
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 Code | HTTP | JSON-RPC | Use When |
|---|
INVALID_ARGUMENT | 400 | -32602 | Bad input from user |
NOT_FOUND | 404 | -32601 | Resource doesn’t exist |
ALREADY_EXISTS | 409 | -32003 | Trying to create duplicate |
PERMISSION_DENIED | 403 | -32004 | User can’t do this |
UNAUTHENTICATED | 401 | -32011 | User not logged in |
RESOURCE_EXHAUSTED | 429 | -32005 | Rate limited |
FAILED_PRECONDITION | 412 | -32006 | System not ready |
INTERNAL | 500 | -32603 | Server bug |
UNAVAILABLE | 503 | -32009 | Service down |
UNIMPLEMENTED | 501 | -32601 | Feature 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:
| Code | HTTP | Use When |
|---|
CANCELED | 499 | Request was canceled by client |
UNKNOWN | 500 | Error doesn’t fit other categories |
DEADLINE_EXCEEDED | 504 | Operation timed out |
ABORTED | 409 | Concurrent modification conflict |
OUT_OF_RANGE | 400 | Pagination or index out of bounds |
DATA_LOSS | 500 | Unrecoverable 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")
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 admin@company.com 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