Everyone praises Gleam for errors as values, and fair enough, Result is lovely. But for data modeling the feature I lean on harder is opaque.
pub opaque type Email { Email(String) } means other modules can see the type and can't call the constructor. The only way to get an Email is through whatever the module exports, say fn parse(raw: String) -> Result(Email, EmailError). So the Result shows up once, at the edge, and everything downstream takes an Email and never checks again.
Without opaque you still get the Result at the edge and no guarantee afterward, because anybody can write Email("lol") and skip the check. The error value tells you a check failed. The opaque type tells you a check happened.
Elm gets there by not exposing the constructor from the module, same idea with less ceremony. I like that Gleam spells it on the type itself, right where you read the definition.