Error taxonomy
Every error from the config stack is a ConfigError with one of seven normatively fixed reason values. The structure is identical across all bindings:
ConfigError {
path: string # dot-notation (database.host)
reason: ConfigErrorReason # enum
details: string # human-readable message
source_id?: string # yaml:/app-config.yaml, etc.
}
The full ConfigErrorReason enum
The normative source is config-spec/_meta/error_reasons.yaml.
missing
The path does not exist in the config and no default was passed to the getter.
- Python
- TypeScript
- Go
config.get_string("nonexistent.path")
# ConfigError(
# path="nonexistent.path",
# reason="missing",
# details="Key 'nonexistent.path' not found in config and no default provided",
# )
config.getString("nonexistent.path");
// Throws ConfigError {
// path: "nonexistent.path",
// reason: "missing",
// details: "Key 'nonexistent.path' not found in config and no default provided",
// }
_, err := cfg.GetString("nonexistent.path")
// err: *config.Error {
// Path: "nonexistent.path",
// Reason: config.ReasonMissing,
// Details: "Key 'nonexistent.path' not found in config and no default provided",
// }
How to handle: check the spelling of the path, add a default argument, or make sure the key exists in app-config.yaml.
type_mismatch
The value exists but is incompatible with the requested type.
- Python
- TypeScript
- Go
# YAML: pool_size: "twenty"
config.get_int("database.pool_size")
# ConfigError(
# path="database.pool_size",
# reason="type_mismatch",
# details="Expected int, got string 'twenty' (does not match ^-?\\d+$)",
# )
// YAML: pool_size: "twenty"
config.getInt("database.pool_size");
// Throws ConfigError {
// path: "database.pool_size",
// reason: "type_mismatch",
// details: "Expected int, got string 'twenty' (does not match ^-?\\d+$)",
// }
// YAML: pool_size: "twenty"
_, err := cfg.GetInt("database.pool_size")
// err: *config.Error {
// Path: "database.pool_size",
// Reason: config.ReasonTypeMismatch,
// Details: "expected int, got string (does not match ^-?\\d+$)",
// }
How to handle: change the YAML value to one that matches the expected type. Interpolated env values are always strings — make sure the string representation matches the regex for the type:
int:^-?\d+$float: standard floating-point parserbool:true|false|yes|no|1|0(case-insensitive)
env_unresolved
The env variable in ${VAR} is not set and no default was specified.
# app-config.yaml:
database:
password: "${DB_PASSWORD}"
If DB_PASSWORD is unset:
ConfigError(
path="database.password",
reason="env_unresolved",
details="Environment variable DB_PASSWORD is not set and no default provided",
)
How to handle: set the env variable (export DB_PASSWORD=...), or — only for non-secret values — add a default to the YAML (${LOG_LEVEL:-INFO}). For secrets without a default, this is a safety net: a missing env variable raises an explicit error at startup.
validation_failed
A typed section read (get_section / getSection / GetSection) did not pass schema validation.
- Python
- TypeScript
- Go
class DatabaseConfig(BaseModel):
host: str
password: str = Field(..., min_length=1)
# YAML: password: "" (empty string)
config.get_section("database", DatabaseConfig)
# ConfigError(
# path="database.password",
# reason="validation_failed",
# details="String should have at least 1 character",
# )
const DatabaseConfigSchema = z.object({
host: z.string(),
password: z.string().min(1),
});
// YAML: password: "" (empty string)
config.getSection("database", DatabaseConfigSchema);
// Throws ConfigError {
// path: "database",
// reason: "validation_failed",
// details: "schema validation failed: String must contain at least 1 character(s)",
// }
type DatabaseConfig struct {
Host string `yaml:"host" validate:"required"`
Password string `yaml:"password" validate:"required,min=1"`
}
// YAML: password: "" (empty string)
var dbCfg DatabaseConfig
err := cfg.GetSection("database", &dbCfg)
// err: *config.Error {
// Path: "database.password",
// Reason: config.ReasonValidationFailed,
// Details: "field Password failed 'min' validation",
// }
How to handle: fix the YAML value; before deploying, add a staging-time check that runs Config.load() plus get_section / getSection / GetSection for every section.
parse_error
The YAML file does not parse (a syntax error).
# Invalid YAML:
database:
host: "localhost
port: 5432
ConfigError(
path="",
reason="parse_error",
details="YAML parse error at line 3: unexpected end of stream",
source_id="yaml:/app/app-config.yaml",
)
How to handle: validate the YAML through yamllint or yq and fix the syntax. An IDE with a YAML linter shows errors immediately.
source_unavailable
A ConfigSource could not load its data.
- Python
- TypeScript
- Go
# The file does not exist:
Config.load("non-existent.yaml")
# ConfigError(
# path="",
# reason="source_unavailable",
# details="File '/app/non-existent.yaml' does not exist",
# source_id="yaml:/app/non-existent.yaml",
# )
// The file does not exist:
await Config.load("non-existent.yaml");
// Throws ConfigError {
// path: "",
// reason: "source_unavailable",
// details: "cannot read non-existent.yaml: ENOENT: no such file or directory",
// sourceId: "yaml:non-existent.yaml",
// }
// The file does not exist:
_, err := config.Load(context.Background(), "non-existent.yaml")
// err: *config.Error {
// Path: "",
// Reason: config.ReasonSourceUnavailable,
// Details: "open non-existent.yaml: no such file or directory",
// SourceID: "yaml:non-existent.yaml",
// }
Other examples planned for Phase 2+ sources:
EtcdSource: connection timeout →source_unavailable.VaultSource: auth failure →source_unavailable.HttpSource: 5xx →source_unavailable.
How to handle: confirm the file exists / the service is reachable / the credentials are correct.
reload_rejected
A hot reload was not applied because validation failed on the new config version.
ConfigError(
path="database.pool_size",
reason="reload_rejected",
details="New config rejected: database.pool_size expected number, got string 'invalid'",
)
This is logged, not thrown — the reload is rolled back atomically, subscribers are not notified, and the application keeps running with the previous values. The diagnostic warning in the log is for the operator.
Hierarchy in each binding
- Python
- TypeScript
- Go
from dagstack.config import ConfigError, ConfigErrorReason
try:
config = Config.load("app-config.yaml")
db_cfg = config.get_section("database", DatabaseConfig)
except ConfigError as exc:
match exc.reason:
case ConfigErrorReason.missing:
log.error(f"Config does not contain {exc.path}")
case ConfigErrorReason.env_unresolved:
log.error(f"Env variable for {exc.path} is not set")
case ConfigErrorReason.validation_failed:
log.error(f"Invalid config at {exc.path}: {exc.details}")
case _:
log.error(f"Config error: {exc}")
raise
import { ConfigError, ConfigErrorReason } from "@dagstack/config";
try {
const config = await Config.load("app-config.yaml");
const dbCfg = config.getSection("database", DatabaseConfig);
} catch (exc) {
if (exc instanceof ConfigError) {
switch (exc.reason) {
case ConfigErrorReason.missing:
log.error(`Config does not contain ${exc.path}`);
break;
case ConfigErrorReason.env_unresolved:
log.error(`Env variable for ${exc.path} is not set`);
break;
case ConfigErrorReason.validation_failed:
log.error(`Invalid config at ${exc.path}: ${exc.details}`);
break;
}
}
throw exc;
}
import (
"context"
"errors"
"go.dagstack.dev/config"
)
cfg, err := config.Load(context.Background(), "app-config.yaml")
if err != nil {
var cfgErr *config.Error
if errors.As(err, &cfgErr) {
switch cfgErr.Reason {
case config.ReasonMissing:
log.Errorf("Config does not contain %s", cfgErr.Path)
case config.ReasonEnvUnresolved:
log.Errorf("Env variable for %s is not set", cfgErr.Path)
case config.ReasonValidationFailed:
log.Errorf("Invalid config at %s: %s", cfgErr.Path, cfgErr.Details)
}
}
return err
}
Secret masking in error messages
Fields from the scrub list (api_key, *_token, *_password, *_secret, ...) are automatically masked inside details:
ConfigError(
path="database.password",
reason="validation_failed",
details="Password '[MASKED]' does not match required pattern", # the actual value is hidden
)
See Secrets.
See also
- ADR-0001 §5 Error model — the normative contract.
- Sources (ConfigSource) —
source_unavailabledepends on the source type. - Hot reload —
reload_rejectedand atomic rollback.