zoobzio December 26, 2025 Edit this page

Errors

Cereal returns wrapped errors with context. This reference covers error types, causes, and handling patterns.

Error Categories

Construction Errors

Returned by NewProcessor:

ErrorCause
invalid tag formatMalformed struct tag (e.g., store.encrypt: without value)
unknown boundaryUnrecognized boundary prefix (not receive/load/store/send)
unknown operationInvalid 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:

ErrorCause
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:

ErrorCause
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:

ErrorCause
invalid key sizeAES key not 16, 24, or 32 bytes
ciphertext too shortDecryption input shorter than nonce
authentication failedGCM tag verification failed (wrong key or corrupted data)
message too longRSA 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:

ErrorCause
bcrypt: cost out of rangeCost < 4 or > 31
argon2: invalid parametersZero values for required params

Hashers rarely fail during operation. SHA hashers never return errors.

Masker Errors

From built-in maskers (ErrMask):

TypeValidation Failure
ssnNot exactly 9 digits
emailMissing @ or @ at position 0
phoneFewer than 7 digits
cardOutside 13-19 digit range
ipInvalid IPv4 or IPv6 format
uuidWrong segment count or lengths (must be 8-4-4-4-12)
ibanOutside 15-34 chars or doesn't start with 2 letters
nameEmpty 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.