[HN Gopher] Patterns for Defensive Programming in Rust
___________________________________________________________________
Patterns for Defensive Programming in Rust
Author : PaulHoule
Score : 160 points
Date : 2025-12-05 16:34 UTC (6 hours ago)
(HTM) web link (corrode.dev)
(TXT) w3m dump (corrode.dev)
| empath75 wrote:
| This is one of the best Rust articles I've ever read. It's
| obviously from experience and covers a lot of _business logic_
| foot guns that Rust doesn't typically protect you against without
| a little bit of careful coding that allows the compiler to help
| you.
|
| So many rust articles are focused on people doing dark sorcery
| with "unsafe", and this is just normal every day api design,
| which is far more practical for most people.
| brohee wrote:
| The very useful TryFrom trait landed only in 1.34, so hopefully
| the code using unwrap_or_else() in From impl predates that...
|
| Actually the From trait documentation is now extremely clear
| about when to implement it (https://doc.rust-
| lang.org/std/convert/trait.From.html#when-t...)
| rolandog wrote:
| As someone unfamiliar with Rust (yet! it's on my ever growing
| list of things I'd like to absorb into my brain),
| unwrap_or_else() sounds like part of the "What You See Is What
| I Threatened the Computer To Do" paradigm.
| Y_Y wrote:
| > INTERCAL has many other features designed to make it even
| more aesthetically unpleasing to the programmer: it uses
| statements such as "READ OUT", "IGNORE", "FORGET", and
| modifiers such as "PLEASE". This last keyword provides two
| reasons for the program's rejection by the compiler: if
| "PLEASE" does not appear often enough, the program is
| considered insufficiently polite, and the error message says
| this; if it appears too often, the program could be rejected
| as excessively polite.
| strbean wrote:
| Immediately thought of INTERCAL :)
| wongarsu wrote:
| There are also the equally threatening and useful
| `map_or_else` (on Result and Option) and `ok_or_else` (on
| Option and experimentally on bool)
| J_Shelby_J wrote:
| Wow that's amazing. The partial equality implementation is really
| surprising.
|
| One question about avoiding boolean parameters, I've just been
| using structs wrapping bools. But you can't treat them like
| bools... you have to index into them like wrapper.0.
|
| Is there a way to treat the enum style replacement for bools like
| normal bools, or is just done with matches! Or match statements?
|
| It's probably not too important but if we could treat them like
| normal bools it'd feel nicer.
| jvanderbot wrote:
| I almost always prefer enums and matches! vs bool parameters.
| Another way is to implement a Trait that you find useful that
| encapsulates the logic. And don't forget you can do impl <Enum>
| {} blocks to add useful functions that execute regardless of
| which member of the enum you got. enum
| MyType{ ... }
| impl MyType{ pub fn is_useable_in_this_way(&self)
| -> bool{ // possibly ... match
| self {...} } }
|
| and later: pub fn use_in_that_way(e: MyType)
| { if e.is_useable_in_this_way() {...} }
|
| Or if you hate all that there's always: if
| let MyType::Member(x) = e { ... }
| J_Shelby_J wrote:
| If let is probably the closest to a regular bool.
|
| For ints you can implement the deref trait on structs. So you
| can treat YourType(u64) as a u64 without destructing. I
| couldn't figure out a way to do that with YouType(bool).
| ggirelli wrote:
| Loved the article, such a nice read. I am still slowly ramping up
| my proficiency in Rust and this gave me a lot of things to think
| through. I particularly enjoyed the temporary mutability pattern,
| very cool and didn't think about it before!
| aw1621107 wrote:
| > I particularly enjoyed the temporary mutability pattern, very
| cool and didn't think about it before!
|
| It's not too uncommon in other languages (sometimes under the
| name "immediately invoked function expression"), though
| depending on the language you may see lambdas involved. For
| example, here's one of the examples from the article ported to
| C++: auto data = []() { auto data
| = get_vector(); auto temp = compute_something();
| data.insert_range(data.end(), temp);
| std::ranges::sort(data); return data; }();
| stouset wrote:
| Good article, but one (very minor) nit I have is with the
| PizzaOrder example. struct PizzaOrder {
| size: PizzaSize, toppings: Vec<Topping>,
| crust_type: CrustType, ordered_at: SystemTime,
| }
|
| The problem they want to address is partial equality when you
| want to compare orders but ignoring the ordered_at timestamp. To
| me, the problem is throwing too many unrelated concerns into one
| struct. Ideally instead of using destructuring to compare only
| the specific fields you care about, you'd decompose this into two
| structs: #[derive(PartialEq, Eq)]
| struct PizzaDetails { size: PizzaSize,
| toppings: Vec<Topping>, crust_type: CrustType,
| ... // additional fields } #[derive(Eq)]
| struct PizzaOrder { details: PizzaDetails,
| ordered_at: SystemTime, } impl PartialEq for
| PizzaOrder { fn eq(&self, rhs: &Self) -> bool {
| self.details == rhs.details } }
|
| I get that this is a toy example meant to illustrate the point;
| there are certainly more complex cases where there's no clean
| boundary to split your struct across. But this should be the
| first tool you reach for.
| hurril wrote:
| You have a good point there, that is better. But it is still,
| well honestly, wrong. Two orders ordered at different times are
| just not the same order, and using a typeclass approach to say
| that they most definitely are is going to bite you in the back
| seat.
|
| PartialEq and Eq for PizzaDetails is good. If there is a
| business function that computes whether or not someone orders
| the same thing, then that should start by projecting the
| details.
| zozbot234 wrote:
| You can solve this in the general case by implementing the
| typeclass for the coarser equality relation over an ad-hoc
| wrapper newtype.
| hurril wrote:
| Well it isn't a good call. This is the kind of code that
| OOP makes people write.
| stouset wrote:
| I do agree that implementing PartialEq on orders in this way
| is a bad fit. But it _is_ a synthetic example to make a
| point, so I tried to keep it in the spirit of the original
| article (while ironically picking nits in the same vein
| myself).
| tialaramex wrote:
| Yeah, I immediately twitched when I saw the PartialEq
| implementation. Somebody is going to write code which finds
| the "correct" order and ends up allowing someone to order the
| same pizza but get yours, while you have to wait for it to be
| made and cooked again.
|
| It's not difficult to write the predicate same_details_as()
| and then it's obvious to reviewers if that's what we meant
| and discourages weird ad-hoc code which might stop working
| when the PizzaDetails is redefined.
| perching_aix wrote:
| This made me wonder, why aren't there usually teams whose job is
| to keep an eye on the coding patterns used in the various
| codebases? Similarly like how you have an SOC team who keeps
| monitoring traffic patterns, or an Operations Support team who
| keeps monitoring health probes, KPIs, and logs, or a QA who keeps
| writing tests against new code, maybe there would be value to
| keeping track of what coding patterns develop into over the
| course of the lifetime of codebases?
|
| Like whenever I read posts like this, they're always fairly
| anecdotal. Sometimes there will even be posts about how large
| refactor x unlocked new capability y. But the rationale always
| reads somewhat retconned (or again, anecdotal*). It seems to me
| that maybe such continuous meta-analysis of one's own codebases
| would have great potential utility?
|
| I'd imagine automated code smell checking tools can only cover so
| much at least.
|
| * I hammer on about anecdotes, but I do recognize that sentiment
| matters. For example, if you're planning work, if something just
| _sounds_ like a lot of work, that 's already going to be
| impactful, even if that judgement is incorrect (since that
| misjudgment may never come to light).
| vlovich123 wrote:
| There are. All the big tech companies have them. It's just
| difficult to accomplish when you have millions of lines of
| code.
| perching_aix wrote:
| Is there an industry standard name for these teams that I
| somehow missed then?
| tczMUFlmoNk wrote:
| You may wish to search for "readability at Google". Here is
| one article:
|
| https://www.moderndescartes.com/essays/readability/
|
| (I have not read this article closely, but it is about the
| right concept, so I provide it as a starting point since
| "readability" writ large can be an ambiguous term.)
| mattarm wrote:
| See https://abseil.io/tips/ for some idea of the kinds of
| guidance these kinds of teams work to provide, at least
| at Google. I worked on the "C++ library team" at Google
| for a number of years.
|
| These roles don't really have standard titles in the
| industry, as far as I'm aware. At Google we were part of
| the larger language/library/toolchain infrastructure org.
|
| Much of what we did was quasi-political ... basically
| coaxing and convincing people to adopt best practices,
| after first deciding what those practices are. Half of
| the tips above were probably written by interested people
| from the engineering org at large and we provided the
| platform and helped them get it published.
|
| Speaking to the original question, no, there were no
| teams just manually reading code and looking for
| mistakes. If buggy code could be detected in an automated
| way, then we'd do that and attempt to fix it everywhere.
| Otherwise we'd attempt to educate and get everyone to
| level up their code review skills.
| perching_aix wrote:
| This is a really cool insight, thank you!
|
| > Half of the tips above were probably written by
| interested people from the engineering org at large and
| we provided the platform and helped them get it
| published.
|
| Are you aware how those engineers established their
| recommendations? Did they maybe perform case studies? Or
| was it more just a distillation of lived experience type
| of deal?
| schneems wrote:
| This was posted with a (mostly) healthy discussion on lobste.rs,
| here's the link
| https://lobste.rs/s/ouy4dq/patterns_for_defensive_programmin...
| emschwartz wrote:
| Indexing into arrays and vectors is really wise to avoid.
|
| The same day Cloudflare had its unwrap fiasco, I found a bug in
| my code because of a slice that in certain cases went past the
| end of a vector. Switched it to use iterators and will definitely
| be more careful with slices and array indexes in the future.
| 1718627440 wrote:
| Funny, it's really the same thing, why Rust people say we
| should abandon C. Meanwhile in C, it is also common to hand out
| handle instead of indices precisely due to this problem.
| kstrauser wrote:
| It's pretty similar, but writing `for item in container {
| item.do_it() }` is a lot less error prone than the C
| equivalent. The ha-ha-but-serious take is that once you get
| that snippet to compile, there's almost nothing you could
| ever do to break it without also making the compiler scream
| at you.
| Dowwie wrote:
| I'm not reading a solid argument as to not use "..Defaults()"
| because doing so suggests that you may introduce a bug and
| therefore should be explicit about EVERYTHING instead? Ugh. Hard
| disagree.
| quotemstr wrote:
| The tech industry is full of brash but lightly-seasoned people
| resurrecting discredited ideas for contrarianism cred and making
| the rest of us put down monsters we thought we'd slain a long
| time ago.
|
| "Defensive programming" has multiple meanings. To the extent it
| means "avoid using _ as a catch-all pattern so that the compiler
| nags you if someone adds an enum arm you need to care about",
| "defensive" programming is good.
|
| That said, I wouldn't use the word "defensive" to describe it.
| The term lacks precision. The above good practice ends up getting
| mixed up with the bad "defensive" practices of converting
| contract violations to runtime errors or just ignoring them
| entirely --- the infamous pattern in Java codebases of scrawling
| the following like of graffiti all over the clean lines of your
| codebase: if (someArgument == null) {
| throw new NullPointerException("someArgument cannot be null");
| }
|
| That's just noise. If someArgument can't be null, let the program
| crash.
|
| Needed file not found? Just return ""; instead.
|
| Negative number where input must be contractually not negative?
| Clamp to zero.
|
| Program crashing because a method doesn't exist? if not:
| hasattr(self, "blah") return None
|
| People use the term "defensive" to refer to code like the above.
| They programs that "defend" against crashes by misbehaving. These
| programs end up being flakier and harder to debug than programs
| that are "defensive" in that they continually validate their
| assumptions and _crash_ if they detect a situation that should be
| impossible.
|
| The term "defensive programming" has been buzzing around social
| media the past few weeks and it's essential that we be precise
| that
|
| 1) constraint verification (preferably at compile time) is good;
| and
|
| 2) avoidance of crashes at runtime at all costs after an error
| has occurred is harmful.
| kstrauser wrote:
| For a second I thought you were advocating for something of
| those, and I had a rant primed up.
|
| Yes. Defensively handle all the failure modes you know how to
| handle, _but nothing else_. If you 're writing a service daemon
| and the user passes in a config filename that doesn't exist,
| _crash_ and say why. Don 't try to guess, or offer up a default
| config, or otherwise try to paper over the idea that the user
| asked you to do something impossible. Pretty much anything you
| try other than just crashing is guaranteed to be wrong.
|
| And for the love of Knuth, don't freaking clamp to zero or
| otherwise convert inputs into semantically different value than
| specified. (Like, it's fine to load a string representation of
| a float into an IEEE754 datatype if you're not working with
| money or other exact values. But don't parse 256 as 255 and
| call it good enough. It isn't.)
| Sunscratch wrote:
| Nice article. The problem of multiple booleans is just one
| instance of a more general problem: when a function takes
| multiple arguments of the same type (i32, String, etc.). The
| newtype pattern allows you to create distinct types in such cases
| and enforce correctness at compile time.
| lilyball wrote:
| In Pattern: Defensively Handle Constructors, it recommends using
| a nested inner module with a private Seal type. But the Seal type
| is unnecessary. The nested inner module is all you need, a
| private field `_private: ()` is only settable from within that
| inner module, so you don't need the extra private type.
| thatoneengineer wrote:
| Aside from just being immensely satisfying, these patterns of
| defensive programming may be a big part of how we get to quality
| GAI-written code at scale. Clippy (and the Rust compiler proper)
| can provide so much of the concrete iterative feedback an agent
| needs to stay on track and not gloss over mistakes.
___________________________________________________________________
(page generated 2025-12-05 23:00 UTC)