We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Errors Are Values step 14 of 24
Implementing std::error::Error
Retire Result<_, String>. Build a real error type and implement
std::error::Error for it by hand.
pub enum ValidationError {
Empty,
TooLong { len: usize, max: usize },
BadChar { ch: char, index: usize },
}
pub fn validate(name: String, max: usize) -> Result<String, ValidationError>
validate returns the name unchanged if it passes, and otherwise the first
rule it breaks — checked in this order:
-
empty →
Empty -
more than
maxcharacters (not bytes) →TooLong { len, max } -
any character that is not alphanumeric and not
_→BadChar { ch, index }with the zero-based character index of the first offender
The Display strings are the contract
The tests assert e.to_string() exactly:
| variant | Display |
|---|---|
Empty |
name must not be empty |
TooLong { len: 11, max: 5 } |
name is 11 characters, maximum is 5 |
BadChar { ch: '-', index: 2 } |
invalid character '-' at index 2 |
They follow the std convention, which is worth adopting permanently:
lowercase, no trailing punctuation, and concise — “unexpected end of file”,
not “Unexpected end of file!”. The reason is composition: your message will be
printed inside somebody else’s sentence, after a :, in a log line, or in a
chain of causes. A capital letter and a full stop in the middle of that reads
like a typo.
The tests also assert format!("{e:?}"), so the variant and field names in the
declaration above must be exactly as written.
Read the starter’s E0277
error[E0277]: `ValidationError` doesn't implement `std::fmt::Display`
|
| impl std::error::Error for ValidationError {}
| ^^^^^^^^^^^^^^^ `ValidationError` cannot be
| formatted with the default
| formatter
= note: required by a bound in `std::error::Error`
That is the whole trait, right there in the error message. Here is its actual declaration:
pub trait Error: Debug + Display {
fn source(&self) -> Option<&(dyn Error + 'static)> { None }
// ...plus some deprecated and unstable methods you should ignore
}
Two supertraits, no required methods. So a minimal implementation is exactly three things:
-
#[derive(Debug)]— for programmers and forunwrap‘s panic message; -
a hand-written
impl Display— for humans; -
impl std::error::Error for MyError {}— an empty block that says “this is an error”, enablingBox<dyn Error>,?conversions andsource()chains.
There is nothing else. If that feels anticlimactic, good — it is why
thiserror can generate the whole thing from an attribute, and why you should
understand it before you use thiserror.
Writing the Display impl
use std::fmt;
impl fmt::Display for ValidationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ValidationError::Empty => write!(f, "name must not be empty"),
// ...
}
}
}
write!(f, …) returns fmt::Result, so the match arms are the return value
— no semicolons, no explicit Ok(()).
Two errors you may meet on the way:
-
E0407, “method
descriptionis not a member of traitError“. Old blog posts and Stack Overflow answers implementfn description(&self) -> &str. It was deprecated in 1.42 and removed from the trait’s required surface. Do not write it. -
E0599 if you call
.to_string()on your error without aDisplayimpl —ToStringis blanket-implemented for everyT: Displayand nothing else.
The rules the API guidelines add
-
Never use
()as an error type. You already metclippy::result_unit_err. -
Error types should be
Send + Sync + 'static. This enum is all three for free (it holds onlyusizeandchar), but the moment you put a non-Sendtype in a variant you cut your callers off fromthread::spawnand from most async runtimes. -
Displaydescribes this error and nothing else. It never renders the cause chain. That is whatsource()is for, and a caller walks it. If you bake the cause into your ownDisplay, every chain-rendering caller prints it twice. This becomes very concrete two items from now.
Notes
-
clippy::error_impl_error(restriction, off here) objects to naming a typeError—ValidationErroris fine,Errorin your own module is confusing. -
chars().count()is O(n) and gives characters;len()is O(1) and gives bytes."héllo"is 5 characters, 6 bytes. The tests check that you picked the right one. -
char::is_alphanumericis Unicode-aware, so"日本語"passes. That is usually what you want for a human-facing name field.
Remember the grade is compile + tests + clippy -D warnings.
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.