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 au16, 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 asIo.
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¶
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¶
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¶
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:orjson:. 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¶
- API reference: which method returns which error
- Limitations: the failures that are silent rather than errors