---
title: AssetError reference
description: Every AssetError variant, the diagnostic code it carries, the message it renders, and what causes it.
tags: [reference, errors, diagnostics]
---
# `AssetError` reference
`AssetError` is the only error type the crate produces. It is
`#[non_exhaustive]`, derives `thiserror::Error` and
`miette::Diagnostic`, and every variant carries a stable diagnostic
code under the `rtb::assets::` namespace.
```rust
#[non_exhaustive]
pub enum AssetError {
NotFound(String),
NotUtf8 { path: String },
Parse { path: String, format: &'static str, message: String },
}
```
Because the enum is `#[non_exhaustive]`, a `match` on it needs a `_`
arm; a new variant in a future release will not break your build.
Only three methods return a `Result`: `open_text`, `load_merged_yaml`
and `load_merged_json`. `open`, `exists` and `list_dir` cannot fail —
they return `None`, `false` and an empty `Vec` respectively.
## `NotFound`
```text
asset not found: templates/readme.md
```
Diagnostic code `rtb::assets::not_found`. The payload is the requested
path, unchanged.
Raised when **no** registered layer provides the path. Causes, in
rough order of likelihood:
- The path does not match how the asset is keyed. Embedded and
in-memory layers compare keys literally, so a leading `./`, a leading
`/`, or a backslash separator will not match.
- The layer you expected to hold it was never registered, or was
registered with its arguments swapped — print the `Assets` with
`{:?}` and check the labels.
- A `DirectorySource` root does not exist. That is not an error in
itself; the layer simply contributes nothing.
- The path names a directory. Directories are listable, not readable.
## `NotUtf8`
```text
asset `icons/app.png` is not valid UTF-8
```
Diagnostic code `rtb::assets::not_utf8`. Raised only by `open_text`,
when a layer provided bytes that are not valid UTF-8.
The bytes are discarded rather than lossily converted. Read binary
assets with `open`, which returns `Option<Vec<u8>>` and never inspects
the contents.
A UTF-8 byte-order mark does not trigger this — the BOM is valid UTF-8
and arrives as a leading `\u{feff}` in the returned `String`. Strip it
yourself if the consumer of the string will not tolerate it.
## `Parse`
```text
failed to parse asset `cfg.yaml (layer `user`)` as YAML: did not find expected key
```
Diagnostic code `rtb::assets::parse`, with the help text "verify the
file is well-formed YAML" (or JSON). Three fields:
| `path` | either `<path> (layer `<label>`)` for a per-layer failure, or the bare `<path>` for a failure of the merged result |
| `format` | `"YAML"` or `"JSON"` |
| `message` | the underlying parser's message, verbatim |
Raised in three distinct situations, which the `path` field
distinguishes:
1. **A contributing layer's file is malformed.** `path` names the
layer. The load aborts; there is no fallback to lower layers.
2. **A contributing layer's YAML bytes are not UTF-8.** Also names the
layer, also reported as a parse failure rather than `NotUtf8`.
3. **The merged document does not fit `T`.** `path` is bare, because
merging has already happened and no single layer can be blamed. A
missing required field, a number out of range for its type, or a
type mismatch all land here.
The `path` field is a human-readable description, not a path you can
feed back into `open`. If you need the clean path for a retry or a log
field, keep hold of the string you passed in.
## Rendering an `AssetError`
`Display` gives the one-line message above. `miette`'s report handler
gives the code and help text as well, which is what a CLI built on the
toolkit will print:
```rust
match assets.load_merged_yaml::<Config>("config.yaml") {
Ok(config) => config,
Err(e) => return Err(miette::Report::new(e)),
}
```
The diagnostic codes are part of the public contract and are safe to
match on or document in a tool's own troubleshooting guide.
## What is deliberately not an error
- **A missing layer root.** A `DirectorySource` pointed at a directory
the user has not created behaves as an empty layer.
- **A rejected path.** `../../etc/passwd` returns `None` from `open`
and `NotFound` from `open_text` — the same as any absent file, with
no indication that a security rule fired. See
[Why path traversal is rejected lexically](../explanation/path-safety.md)
for the reasoning.
- **An unreadable file.** Permission denied is indistinguishable from
absent, and a lower layer gets its turn.
- **An empty overlay.** `Assets::default()` is valid and every read
simply finds nothing.