Skip to main content

Why Error Handling Matters

When building APIs, errors are inevitable. Users will request items that don’t exist, provide invalid data, or lack permissions. How you handle these errors determines whether your API is frustrating or pleasant to use. Contract provides a portable error system that ensures your errors work correctly across REST, JSON-RPC, MCP, and other protocols - without you having to think about each one separately.

The Problem with Simple Errors

Consider this code in a todo service:
The problem? Different protocols expect errors in different formats: With a simple errors.New(), Contract can’t know which HTTP status to use, so it defaults to 500 Internal Server Error - even though β€œnot found” should clearly be 404.

The Solution: Contract Errors

Contract provides error types that know how to translate themselves to each protocol:
This single line automatically becomes the right format for each protocol: You write one error, and it works everywhere.

Creating Errors

Contract provides convenient functions for common error types:

Method 2: NewError with Code

For more control, use NewError with an explicit error code:

Method 3: Errorf with Formatting

Use Errorf when you need to include variables in the message:

Choosing the Right Error Code

User’s Fault (4xx HTTP Status)

Use these when the client made a mistake or is requesting something they can’t have:

Your Fault (5xx HTTP Status)

Use these when something went wrong on your end:

Error Code Reference

Adding Context to Errors

Adding Details

Details help clients understand and handle errors better. They appear in the error response but don’t change the message:
Details appear in the JSON response:
This is incredibly useful for:
  • Frontend developers: Can show β€œPlease enter a valid email address” next to the email field
  • API consumers: Can programmatically handle specific error cases
  • Debugging: Includes context about what went wrong

Wrapping Underlying Errors

Preserve the original error for debugging while showing a user-friendly message:
The WithCause method:
  • Preserves the original error for logging and debugging
  • Hides internal details from API responses (security!)
  • Supports errors.Is() and errors.As() for checking causes

Complete Example

Here’s a realistic service with proper error handling throughout:

Best Practices

1. Use Specific Error Codes

Specific codes map to correct HTTP status codes:

2. Write User-Friendly Messages

Messages should tell users what went wrong and what to do:

3. Include Relevant Details

Details help clients handle errors intelligently:

4. Don’t Leak Internal Details

Protect your infrastructure from exposure:

5. Handle Each Error Case

Map different underlying errors to appropriate codes:

Testing Errors

Test that your errors have the correct code and details:

Common Questions

What happens if I return a regular Go error?

Regular errors are treated as Internal errors (HTTP 500):
Rule of thumb: Always use Contract errors for expected error cases. Use regular errors only for unexpected bugs that shouldn’t happen.

Can I create custom error codes?

Contract uses standard codes that map consistently across protocols. For custom error semantics, use the details field:

Should I log errors in my service?

Your service should return errors, not log them. Let middleware handle logging. This keeps your service focused and testable:

What’s Next?

Now that you understand error handling: