Alexis King's Parse, don't validate gave a name to an idiom many Rust programmers had already been using without a label. The pattern, originally illustrated in Haskell, shows up repeatedly in the Rust standard library and in well-known projects — which makes it useful to collect the concrete examples.
The starting point is the familiar distinction: a function that only checks a property and returns a boolean or Option leaves the caller to re-check the property later, while a function that returns a new type carrying the property in the type system makes that re-check unnecessary.
Refinement in the standard library
Vec::first returns Option<&T> because a vector may be empty. A function that enforces non-emptiness at its boundary — for instance one that reads configuration directories from an environment variable and rejects an empty list — still returns a Vec, so even a successful result forces the caller through Option handling again:
use anyhow::{Result, ensure};
fn get_configuration_directories() -> Result<Vec<PathBuf>> {
let value = env::var("CONFIG_DIRS").context("could not read CONFIG_DIRS")?;
let directories: Vec<PathBuf> = value
.split(',')
.map(str::trim)
.map(PathBuf::from)
.collect();
ensure!(!directories.is_empty(), "empty CONFIG_DIRS");
Ok(directories)
}
fn main() -> Result<()> {
let config_dirs = get_configuration_directories()?;
match config_dirs.first() {
Some(cache_dir) => initialize_cache(cache_dir),
None => unreachable!("already checked that CONFIG_DIRS is non-empty"),
}
Ok(())
}
The invariant lives only in a comment. If get_configuration_directories ever changes, nothing in the type system notices. Clarity, performance, and a subtle time bomb all suffer.
A type that cannot be empty
Such crates as nonempty replace the comment with a type. NonEmpty has no constructor permitting zero elements: new takes one element, and first returns &T directly.
pub struct NonEmpty<T> {
pub head: T,
pub tail: Vec<T>,
}
pub const fn new(e: T) -> Self {
Self::singleton(e)
}
pub const fn singleton(head: T) -> Self {
NonEmpty {
head,
tail: Vec::new(),
}
}
pub const fn first(&self) -> &T {
&self.head
}
Traits and conversions bring NonEmpty close to a regular Vec in behavior:
pub fn from_vec(mut vec: Vec<T>) -> Option<NonEmpty<T>> {
if vec.is_empty() {
None
} else {
let head = vec.remove(0);
Some(NonEmpty { head, tail: vec })
}
}
Rewriting get_configuration_directories to return NonEmpty moves the check into the constructor:
fn get_configuration_directories() -> Result<NonEmpty<PathBuf>> {
let value = env::var("CONFIG_DIRS").context("could not read CONFIG_DIRS")?;
let directories = value
.split(',')
.map(str::trim)
.map(PathBuf::from)
.collect();
let Some(directories) = NonEmpty::from_vec(directories) else {
bail!("CONFIG_DIRS cannot be empty");
};
Ok(directories)
}
The call site then reflects the guaranteed invariant — no emptiness test reappears:
fn main() -> Result<()> {
let config_dirs = get_configuration_directories()?;
initialize_cache(config_dirs.first())?;
Ok(())
}
That is the parse/validate split: returning a Vec validates; returning NonEmpty parses the vector into a richer type. Parsing here means transforming data from one format to another, where the target format encodes the invariant.
The Rust rewrite of POSIX utilities applies this to shell pipelines:
pub struct Pipeline {
pub commands: NonEmpty<Command>,
pub negate_status: bool,
}
fn parse_pipeline(&mut self, alias_table: &AliasTable) -> ParseResult<Option<Pipeline>> {
// pipeline = "!" command ("|" linebreak command)*
let negate_status = self.match_alternatives(&[CommandToken::Bang])?.is_some();
let mut commands = if let Some(command) = self.parse_command(alias_table)? {
NonEmpty::new(command)
} else {
return Ok(None);
};
// ...
A Pipeline is returned only when the AST contains commands; otherwise None. Downstream code calls commands.first() without an Option in sight.
Gradual refinement in rust-analyzer
rust-analyzer refines a path once it has been parsed as absolute:
pub struct AbsPathBuf(Utf8PathBuf);
impl TryFrom<Utf8PathBuf> for AbsPathBuf {
type Error = Utf8PathBuf;
fn try_from(path_buf: Utf8PathBuf) -> Result<AbsPathBuf, Utf8PathBuf> {
if !path_buf.is_absolute() {
return Err(path_buf);
}
Ok(AbsPathBuf(path_buf))
}
}
Code after that point need not re-check absoluteness. The same holds for UTF-8 validity: AbsPathBuf wraps Utf8PathBuf from the camino crate, not the standard library's PathBuf. Standard paths have no UTF-8 guarantee, and converting them to String is therefore awkward; camino establishes UTF-8 validity on construction, after which conversion is trivial:
fn as_str(&self) -> &str {
...
}
The pattern here is cumulative: each construction step adds a guarantee the previous type did not have.
std::path::PathBuf
|
| prove UTF-8
|
V
camino::Utf8PathBuf
|
| prove absolute
|
V
rust-analyzer's paths::AbsPathBuf
Non-zero integers and deserialization
The generic NonZero types describe unsigned values known to be non-zero. thread::available_parallelism returns one:
pub fn available_parallelism() -> Result<NonZero<usize>>
A successful call yields a NonZero<usize>, so no caller has to consider a parallelism of zero. Rust also defines division with a NonZero denominator as a non-panicking operation. Beyond validation, NonZero enables a niche optimization: zero is not a valid value, so it can serve as the None bit pattern. Option<NonZeroUsize> therefore occupies the same size and alignment as NonZeroUsize — and as usize — avoiding storage Option<usize> would otherwise need.
Serde-driven JSON deserialization applies the same idea without ceremony:
#[derive(Debug, Deserialize)]
struct Config {
name: String,
workers: NonZeroUsize,
mode: Mode,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "snake_case")]
enum Mode {
Fast,
Safe,
}
let input = r#"
{
"name": "compiler",
"workers": 4,
"mode": "fast"
}
"#;
let config: Config = serde_json::from_str(input)?;
Several guarantees are enforced as the value lands in the type. Field types are checked, so name cannot arrive as an array; mode must be one of the Mode variants; workers must be non-zero because the field is NonZeroUsize. Once parsing converts mode into a Mode, no further validation follows.
Dynamic languages usually require manual work here. Python's json.loads hands back a dictionary and the caller validates it. Pydantic can approximate the Rust approach, but it is not universally adopted.
| [1] | Other languages - like Go or Python - have a runtime check that raises some sort of exception or panic when lst[0] is accessed on an empty list or slice. |
| [2] | The project implements its own NonEmpty, without relying on the nonempty crate, but all the points in this post apply. |



