zoobzio January 23, 2026 Edit this page

Troubleshooting

This guide covers common errors, their causes, and how to resolve them.

Error Types

ErrRequired

Message: fig: required field not set

Cause: A field marked required:"true" has no value from any source (secret, env, or default).

Wrapped in: FieldError with the field name.

Example:

type Config struct {
    APIKey string `env:"API_KEY" required:"true"`
}

var cfg Config
err := fig.Load(&cfg)
// Error: fig: field APIKey: fig: required field not set

Solutions:

  1. Set the environment variable: export API_KEY=your-key
  2. Add a default value: default:"fallback-key"
  3. Provide the value via secret provider
  4. Remove required:"true" if the field is optional

ErrInvalidType

Message: fig: invalid type conversion

Cause: A string value could not be parsed into the target type.

Wrapped in: FieldError with the field name.

Examples:

// "abc" is not a valid integer
type Config struct {
    Port int `env:"PORT"`
}
// PORT=abc → fig: field Port: fig: invalid type conversion

// "maybe" is not a valid boolean
type Config struct {
    Debug bool `env:"DEBUG"`
}
// DEBUG=maybe → fig: field Debug: fig: invalid type conversion

Solutions:

  1. Fix the environment variable value
  2. Use valid formats:
    • Integers: 123, -456
    • Floats: 1.5, -2.7
    • Bools: true, false, 1, 0, t, f
    • Durations: 30s, 5m, 1h30m

ErrNotStruct

Message: fig: type must be a struct

Cause: Load was called with a non-struct type.

Example:

var s string
err := fig.Load(&s)
// Error: fig: type must be a struct

Solution: Pass a pointer to a struct.

ErrSecretNotFound

Message: fig: secret not found

Cause: The secret provider could not find the requested key.

Behaviour: This error is handled gracefully—fig continues to the next source (env, then default). It only becomes a problem if the field is required and no other source provides a value.

When you see it: Typically wrapped in FieldError when a required secret-only field has no value:

type Config struct {
    Password string `secret:"db/password" required:"true"`
}
// No provider, or provider doesn't have "db/password"
// Error: fig: field Password: fig: required field not set

FieldError

All field-level errors are wrapped in FieldError:

type FieldError struct {
    Field string
    Err   error
}

Extracting field information:

err := fig.Load(&cfg)
if err != nil {
    var fieldErr *fig.FieldError
    if errors.As(err, &fieldErr) {
        fmt.Printf("Field: %s\n", fieldErr.Field)
        fmt.Printf("Cause: %v\n", fieldErr.Err)
    }
}

Checking for specific errors:

if errors.Is(err, fig.ErrRequired) {
    // Handle missing required field
}
if errors.Is(err, fig.ErrInvalidType) {
    // Handle type conversion failure
}

Common Issues

Empty Environment Variables

Empty environment variables are treated as unset:

// APP_HOST="" (empty string)
type Config struct {
    Host string `env:"APP_HOST" default:"localhost"`
}
// cfg.Host = "localhost" (falls through to default)

If you need to distinguish between "unset" and "empty string", use a pointer:

type Config struct {
    Host *string `env:"APP_HOST"`
}
// APP_HOST="" → cfg.Host = ptr to ""
// APP_HOST unset → cfg.Host = nil

Unexported Fields

Unexported fields cannot be set:

type Config struct {
    host string `env:"APP_HOST"` // lowercase, unexported
}
// cfg.host remains zero value

Solution: Export the field (capitalise the first letter).

Missing Nested Struct Metadata

Nested structs must be defined in a way that sentinel can scan them:

// Works: named struct type
type DatabaseConfig struct {
    Host string `env:"DB_HOST"`
}

type Config struct {
    Database DatabaseConfig
}

// Doesn't work: anonymous struct
type Config struct {
    Database struct {
        Host string `env:"DB_HOST"`
    }
}

Solution: Use named struct types for nested configuration.

Tagged Nested Structs

If a nested struct field has fig tags, it's treated as a leaf, not a container:

type Config struct {
    // This is treated as a single field, not recursed into
    Database DatabaseConfig `env:"DATABASE_JSON"`
}

Solution: Remove tags from the parent field to enable recursion.

Provider Errors vs Not Found

Providers should return ErrSecretNotFound only when the secret doesn't exist. Other errors (network, permissions) should propagate:

func (p *MyProvider) Get(ctx context.Context, key string) (string, error) {
    val, err := p.client.Get(key)
    if err == client.ErrNotFound {
        return "", fig.ErrSecretNotFound  // fig continues to next source
    }
    if err != nil {
        return "", err  // fig fails with this error
    }
    return val, nil
}

If your provider returns ErrSecretNotFound for all errors, you'll mask real problems.

Debugging Strategies

Add logging to understand which sources are being used:

type loggingProvider struct {
    inner fig.SecretProvider
}

func (p *loggingProvider) Get(ctx context.Context, key string) (string, error) {
    val, err := p.inner.Get(ctx, key)
    if err != nil {
        log.Printf("secret %q: not found", key)
    } else {
        log.Printf("secret %q: found", key)
    }
    return val, err
}

Inspect FieldError Chain

Walk the error chain to understand the full context:

err := fig.Load(&cfg)
if err != nil {
    for e := err; e != nil; e = errors.Unwrap(e) {
        fmt.Printf("%T: %v\n", e, e)
    }
}

Use Struct Literals for Testing

When debugging, create a known-good config to compare against:

expected := Config{
    Host: "localhost",
    Port: 8080,
}

var actual Config
fig.Load(&actual)

if actual != expected {
    t.Errorf("got %+v, want %+v", actual, expected)
}

Next Steps