Skip to content

Error reference

ConfigError is the only error type the crate returns. It derives thiserror::Error and miette::Diagnostic, so every variant carries a diagnostic code in the rtb::config::* namespace and prints with miette's formatting when your main returns miette::Result.

The enum is #[non_exhaustive]. A match over it needs a _ arm, and a future release can add a variant without that being a breaking change.

Which variant means what

Variant Diagnostic code Returned by
Parse(String) rtb::config::parse build, reload
Io { path, source } rtb::config::io build, reload
Watch(String) rtb::config::watch watch_files
Write(String) rtb::config::write write
Schema(String) rtb::config::schema Nothing in the crate today — see below

Watch, Write and Schema exist unconditionally even though the code that constructs them is behind a Cargo feature. That is deliberate: a downstream match does not have to be #[cfg]-gated to compile with the features off.

Parse — the merged configuration did not fit the struct

configuration error: invalid type: found string "not-a-number", expected u16 for key "PORT" in `MYTOOL_` environment variable(s)

The catch-all for anything the merge-and-deserialise step rejects. It carries the underlying message, which names the offending key and the layer it came from.

It covers more ground than the name suggests:

  • Malformed YAML in a user file.
  • A type mismatchport: "eighty" against a u16, whether it came from a file or an environment variable.
  • A missing required field — any field without #[serde(default)] that no layer supplied.
  • An unknown field, but only if your struct carries #[serde(deny_unknown_fields)]. Without it, unknown keys are ignored.
  • A file that exists but cannot be read — a permissions failure arrives here as Parse, wrapping the OS message, not as Io.

The diagnostic help text is check your config file and environment variables against the schema.

Io — a config path exists but is not a regular file

could not read config file /etc/mytool: config path is not a regular file

Narrower than its name suggests. The crate returns Io in exactly one case: a path registered with user_file exists on disk and is not a regular file — a directory being the usual way to get here, typically by passing a config directory where a file was wanted.

It carries the offending path, which is the useful part when several files are registered, and an std::io::Error as its source.

A path that does not exist at all is not an error of any kind. A path that exists and cannot be opened is a Parse error.

Watch — the file watcher could not start

config watcher error: no user files registered

Only ever returned by watch_files, and only at start-up. Three causes:

Message shape Cause Fix
no user files registered The Config was built without any user_file path — including anything built by with_value or default() Register the file you want watched with ConfigBuilder::user_file
watch <path>: No path was found. about ["<path>"] The registered path does not exist yet Create the file before calling watch_files; the watcher cannot wait for a path to appear
debouncer: … / spawn watcher thread: … The OS refused a watch handle or a thread Check inotify limits (fs.inotify.max_user_watches on Linux) and process limits

Once the watcher is running it never returns an error. A reload that fails after a bad save is swallowed on purpose so one broken write does not take the watcher down — but it also means a watcher that has stopped noticing changes reports nothing. See limitations.

Write — writing the config file failed

config write error: create parent /tmp/blocker: File exists (os error 17)

Returned by Config::write under the mutable feature, for both halves of the job:

  • Serialisation failed — the message is prefixed with the format, yaml:, toml: or json:. TOML is the one that realistically fails, because it cannot represent every structure serde can.
  • The filesystem refused — creating the parent directory or writing the file. The message carries the path.

Schema — reserved for schema validation

Nothing in rtb-config constructs this variant today. It exists for consumers that validate a candidate value against Config::schema before writing it — a config set command checking an edit before it lands — so that the failure has a home in the same error type rather than needing a second one.

If you are looking for the error that a bad value in a config file produces, that is Parse.

How to handle these in a tool

ConfigError implements miette::Diagnostic, so the shortest useful handling is to let it propagate:

fn main() -> miette::Result<()> {
    let cfg = rtb_config::Config::<MyConfig>::builder()
        .embedded_default(include_str!("../defaults.yaml"))
        .user_file("/etc/mytool/config.yaml")
        .env_prefixed("MYTOOL_")
        .build()?;
    // ...
    Ok(())
}

That prints the message, the diagnostic code and the help line. Match on the enum only when you want to act differently — for instance treating Io as "the user pointed at a directory, re-prompt" while letting Parse fail the run.

Next