Errors
Cereal returns wrapped errors with context. This reference covers error types, causes, and handling patterns.
Error Categories
Construction Errors
Returned by NewProcessor:
| Error | Cause |
|---|---|
invalid tag format | Malformed struct tag (e.g., store.encrypt: without value) |
unknown boundary | Unrecognized boundary prefix (not receive/load/store/send) |
unknown operation | Invalid operation for boundary (e.g., receive.encrypt) |
proc, err := cereal.NewProcessor[User](json.New())
if err != nil {
// Tag parsing failed - fix struct tags
log.Fatalf("processor creation failed: %v", err)
}
Validation Errors
Returned by Validate:
| Error | Cause |
|---|---|
missing encryptor for algorithm "X" | Field uses store.encrypt:"X" but no encryptor registered |
missing hasher for algorithm "X" | Field uses receive.hash:"X" but no hasher registered |
missing masker for type "X" | Field uses send.mask:"X" but no masker registered |
err := proc.Validate()
if err != nil {
// Configuration incomplete - register missing handlers
log.Fatalf("validation failed: %v", err)
}
Note: SHA-256, SHA-512, and all mask types are registered by default. Only encryption and Argon2/bcrypt require explicit registration.
Operation Errors
Returned by Receive, Load, Store, Send:
| Error | Cause |
|---|---|
unmarshal: ... | Codec failed to parse input bytes |
marshal: ... | Codec failed to serialize output |
encrypt field X: ... | Encryption failed for field |
decrypt field X: ... | Decryption failed for field |
hash field X: ... | Hashing failed for field |
mask field X: ... | Masking failed for field (invalid format) |
user, err := proc.Receive(ctx, data)
if err != nil {
// Check error type for specific handling
var unmarshalErr *json.SyntaxError
if errors.As(err, &unmarshalErr) {
// Invalid JSON input
}
}
Encryptor Errors
From built-in encryptors:
| Error | Cause |
|---|---|
invalid key size | AES key not 16, 24, or 32 bytes |
ciphertext too short | Decryption input shorter than nonce |
authentication failed | GCM tag verification failed (wrong key or corrupted data) |
message too long | RSA plaintext exceeds key size limit |
enc, err := cereal.AES(key)
if err != nil {
// Key size invalid
}
// During operation
_, err = proc.Load(ctx, corruptedData)
// err: "decrypt field Email: authentication failed"
Hasher Errors
From built-in hashers:
| Error | Cause |
|---|---|
bcrypt: cost out of range | Cost < 4 or > 31 |
argon2: invalid parameters | Zero values for required params |
Hashers rarely fail during operation. SHA hashers never return errors.
Masker Errors
From built-in maskers (ErrMask):
| Type | Validation Failure |
|---|---|
ssn | Not exactly 9 digits |
email | Missing @ or @ at position 0 |
phone | Fewer than 7 digits |
card | Outside 13-19 digit range |
ip | Invalid IPv4 or IPv6 format |
uuid | Wrong segment count or lengths (must be 8-4-4-4-12) |
iban | Outside 15-34 chars or doesn't start with 2 letters |
name | Empty or whitespace only |
user := &User{Email: "invalid"}
_, err := proc.Send(ctx, user)
// err: "mask field Email: mask failed: invalid email format, missing or misplaced @"
if errors.Is(err, cereal.ErrMask) {
// Input data doesn't match expected format
}
Error Wrapping
Errors include context via fmt.Errorf with %w:
// Original error
err := enc.Decrypt(ciphertext)
// "authentication failed"
// Wrapped by processor
// "decrypt field User.Email: authentication failed"
Use errors.Is and errors.As to inspect:
if errors.Is(err, someSpecificError) {
// Handle specific case
}
var targetErr *SomeErrorType
if errors.As(err, &targetErr) {
// Access error details
}
Handling Patterns
Fail Fast
For startup configuration:
proc, err := cereal.NewProcessor[User](json.New())
if err != nil {
log.Fatal(err)
}
enc, err := cereal.AES(key)
if err != nil {
log.Fatal(err)
}
proc.SetEncryptor(cereal.EncryptAES, enc)
if err := proc.Validate(); err != nil {
log.Fatal(err)
}
Graceful Degradation
For runtime operations:
func handleRequest(ctx context.Context, body []byte) (*User, error) {
user, err := proc.Receive(ctx, body)
if err != nil {
// Log full error for debugging
log.Printf("receive failed: %v", err)
// Return safe error to caller
return nil, fmt.Errorf("invalid request format")
}
return user, nil
}
Retry with Backoff
For transient failures (e.g., KMS errors):
func loadWithRetry(ctx context.Context, data []byte) (*User, error) {
var user *User
var err error
for attempt := 0; attempt < 3; attempt++ {
user, err = proc.Load(ctx, data)
if err == nil {
return user, nil
}
// Only retry on transient errors
if !isTransient(err) {
return nil, err
}
time.Sleep(time.Duration(attempt*100) * time.Millisecond)
}
return nil, fmt.Errorf("load failed after retries: %w", err)
}
func isTransient(err error) bool {
// KMS timeout, network error, etc.
return strings.Contains(err.Error(), "timeout") ||
strings.Contains(err.Error(), "connection")
}
Field-Level Recovery
Using override interfaces for partial success:
func (u *User) Decrypt(encryptors map[cereal.EncryptAlgo]cereal.Encryptor) error {
enc := encryptors[cereal.EncryptAES]
// Try to decrypt email, leave as-is if failed
if u.Email != "" {
ciphertext, err := base64.StdEncoding.DecodeString(u.Email)
if err == nil {
plaintext, err := enc.Decrypt(ciphertext)
if err == nil {
u.Email = string(plaintext)
}
// Silently skip on decrypt failure (maybe already plaintext)
}
}
return nil // Never fail
}
Nil Pointer Behavior
Fields in nil nested structs are silently skipped:
type User struct {
Profile *Profile `json:"profile"`
}
type Profile struct {
Email string `json:"email" store.encrypt:"aes"`
}
user := &User{Profile: nil}
data, err := proc.Store(ctx, user)
// No error - Profile.Email skipped because Profile is nil
This is by design. Nil checks happen during field traversal; nil pointers short-circuit without error.
Validation Timing
Validation runs automatically on the first operation. Missing handlers are caught immediately:
proc, _ := cereal.NewProcessor[User](json.New())
// Forgot SetEncryptor...
data, err := proc.Store(ctx, &user)
// err: "missing encryptor for algorithm \"aes\" (field Email)"
For earlier detection, call Validate() explicitly at startup:
proc, _ := cereal.NewProcessor[User](json.New())
proc.SetEncryptor(cereal.EncryptAES, enc)
if err := proc.Validate(); err != nil {
log.Fatal(err) // Fail fast at startup
}
Both approaches produce the same error—explicit validation just moves it earlier in the lifecycle.
Override Interface Errors
When implementing override interfaces, return errors to stop the operation:
func (u *User) Encrypt(encryptors map[cereal.EncryptAlgo]cereal.Encryptor) error {
enc, ok := encryptors[cereal.EncryptAES]
if !ok {
return fmt.Errorf("AES encryptor not configured")
}
ciphertext, err := enc.Encrypt([]byte(u.Email))
if err != nil {
return fmt.Errorf("encrypt email: %w", err)
}
u.Email = base64.StdEncoding.EncodeToString(ciphertext)
return nil
}
The processor wraps your error and returns it from the operation.