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 mismatch, such as port: "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. In practice that means a config directory was passed where a file was wanted.

It carries the offending path, which is the useful part when several files are registered, and a 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. The causes:

Message shape Cause Fix
no user files registered The Config was built without any user_file path, which includes anything built by with_value or default() Register the file you want watched with ConfigBuilder::user_file
watch <dir>: No such file or directory (os error 2) The directory holding a registered file does not exist Create the directory before calling watch_files; the file itself may appear later
watch <dir>: OS file watch limit reached. about ["<dir>"] The OS refused another watch On Linux, raise fs.inotify.max_user_watches
debouncer: … / spawn watcher thread: … The OS refused a watcher instance or a thread On Linux, check fs.inotify.max_user_instances, then 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 reload that keeps failing 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, either 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