惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Cloudbric
Cloudbric
T
Threat Research - Cisco Blogs
Simon Willison's Weblog
Simon Willison's Weblog
AWS News Blog
AWS News Blog
P
Privacy & Cybersecurity Law Blog
H
Help Net Security
云风的 BLOG
云风的 BLOG
G
GRAHAM CLULEY
Spread Privacy
Spread Privacy
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
A
Arctic Wolf
Project Zero
Project Zero
Engineering at Meta
Engineering at Meta
P
Privacy International News Feed
Blog — PlanetScale
Blog — PlanetScale
Stack Overflow Blog
Stack Overflow Blog
M
MIT News - Artificial intelligence
The Register - Security
The Register - Security
Recorded Future
Recorded Future
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
C
Cisco Blogs
PCI Perspectives
PCI Perspectives
Recent Announcements
Recent Announcements
Martin Fowler
Martin Fowler
A
About on SuperTechFans
W
WeLiveSecurity
GbyAI
GbyAI
V
Vulnerabilities – Threatpost
The GitHub Blog
The GitHub Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
C
Check Point Blog
Y
Y Combinator Blog
月光博客
月光博客
Scott Helme
Scott Helme
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Google DeepMind News
Google DeepMind News
F
Fortinet All Blogs
U
Unit 42
G
Google Developers Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
Threatpost
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Google Online Security Blog
Google Online Security Blog
Recent Commits to openclaw:main
Recent Commits to openclaw:main
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Cisco Talos Blog
Cisco Talos Blog
博客园 - 三生石上(FineUI控件)
Hugging Face - Blog
Hugging Face - Blog
MongoDB | Blog
MongoDB | Blog
博客园 - 司徒正美

oida.dev | TypeScript, Rust

TypeScript's `erasableSyntaxOnly` Flag Unsafe for work Tokio: Macros Tokio: Channels Tokio: Getting Started Network Applications on the Tokio Stack Remake, Remodel, Reduce. The `never` type and error handling in TypeScript 5 Inconvenient Truths about TypeScript Refactoring in Rust: Introducing Traits Refactoring in Rust: Abstraction with the Newtype Pattern Announcing the TypeScript Cookbook TypeScript: Iterating over objects The road to universal JavaScript 10 years of oida.dev Rust: Tiny little traits The TypeScript converging point How not to learn TypeScript Getting started with Rust Introducing Slides and Coverage TypeScript: The humble function overload TypeScript + React: Children types are broken TypeScript: In defense of any Dissecting Deno Error handling in Rust TypeScript: Unexpected intersections Upgrading Node.js dependencies after a yarn audit TypeScript: Array.includes on narrow types TypeScript + React: Typing Generic forwardRefs shared, util, core: Schroedinger's module names Learning Rust and Go TypeScript: Narrow types in catch clauses TypeScript: Low maintenance types Tidy TypeScript: Name your generics Tidy TypeScript: Avoid traditional OOP patterns Tidy TypeScript: Prefer type aliases over interfaces Tidy TypeScript: Prefer union types over enums My new book: TypeScript in 50 Lessons Go Preact! ❤️ this in JavaScript and TypeScript TypeScript and ECMAScript Modules TypeScript + React: Why I don't use React.FC TypeScript + React: Component patterns TypeScript: Augmenting global and lib.dom.d.ts Vite with Preact and TypeScript TypeScript: Union to intersection type 11ty: Generate Twitter cards automatically Are large node module dependencies an issue? TypeScript: Variadic Tuple Types Preview TypeScript: Improving Object.keys Remake, Remodel. Part 4. TypeScript + React: Typing custom hooks with tuple types TypeScript: Assertion signatures and Object.defineProperty TypeScript: Check for object properties and narrow down type Boolean in JavaScript and TypeScript void in JavaScript and TypeScript Symbols in JavaScript and TypeScript Why I use TypeScript TypeScript + React: Extending JSX Elements TypeScript: Validate mapped types and const context TypeScript: Match the exact object shape TypeScript: The constructor interface pattern Streaming your Meetup - Part 4: Directing and Streaming with OBS Streaming your Meetup - Part 3: Speaker audio Streaming your Meetup - Part 2: Speaker video Streaming your Meetup - Part 1: Basics and Projector TypeScript and React Guide: Added a new styles chapter TypeScript and React Guide: Added a new render props chapter TypeScript and React: Styles and CSS TypeScript and React TypeScript and React Guide: Added a new prop types chapter TypeScript without TypeScript -- JSDoc superpowers TypeScript: Mapped types for type maps JAMStack vs serverless web apps The Unsung Benefits of JAMStack Sites TypeScript: Ambient modules for Webpack loaders My most favourite talks in 2018 TypeScript and React Guide: Added a new context chapter TypeScript: Built-in generic types TypeScript: Type predicates JSX is syntactic sugar TypeScript and React Guide: Added a new hooks chapter Getting your CfP application right FAQ on our Angular Connect Talk: Automating UI development TypeScript and Substitutability Debugging Node.js apps in TypeScript with Visual Studio Code From Medium: Deconfusing Pre- and Post-processing From Medium: PostCSS misconceptions Saving and scraping a website with Puppeteer Cutting the mustard - 2018 edition Wordpress as CMS for your JAMStack sites My most favourite podcast episodes in 2017 My most favourite talks in 2017 My most favourite books in 2017 The Best Request Is No Request, Revisited Not so hidden figures - Organizing ScriptConf My podcast journey to ScriptCast Grid layout, grid layout everywhere! #scriptconf and #devone Object streams in Node.js
Rust: Enums to wrap multiple errors
2021-09-09 · via oida.dev | TypeScript, Rust

This is a follow-up to Error handling in Rust from a couple of days ago. The moment we want to use error propagation for different error types, we have to rely on trait objects with Box<dyn Error>, which means we defer a lot of information from compile-time to runtime, for the sake of convenient error handling.

Which you might consider not convenient at all, because there’s some downcasting involved to get the original error back, and we rely on trait objects and dynamic dispatch to carry something like an error along our codebase. I’d rather have this information erased at compile time!

Memory layout of Box and Box<dyn Trait>
Memory layout of Box and Box<dyn Trait>

There is a really nice pattern to handle multiple errors that involve enums. This is what I want to share with you today. It requires setting up a lot more boilerplate (which surely can be macro’d somehow), but in the end, I find it much better to use, and it arguably has some benefits at runtime as well.

Previously: Trait objects #

Let’s quickly recap what we ended up with in the last example.

use std::error;

fn number_from_file(filename: &str) -> Result<u64, Box<dyn error::Error>> {
/* 1: std::io::Error */
let mut file = File::open(filename)?;

let mut buffer = String::new();

/* 1: std::io::Error */
file.read_to_string(&mut buffer)?;

/* 2: ParseIntError */
let parsed: u64 = buffer.trim().parse()?;

Ok(parsed)
}

This function can cause two different error types.

  1. An std::io::Error when we open the file or read from it
  2. A std::num::ParseIntError when we try to parse the string into a u64

Since both implement the std::error::Error trait, we can use a boxed trait object Box<dyn Error> to propagate the error and have a dynamic result based on what happens in our program. Again: It’s important to iterate that this defines dynamic behavior at runtime, whereas in all other cases Rust tries to figure out as much as possible at compile.

Using enums #

Instead of having a dynamic return result, we prepare an Error enum with all possible errors. In our example, that’s a ParseIntError as well as an std::io::Error.

enum NumFromFileErr {
ParseError(ParseIntError),
IoError(std::io::Error),
}

To use this enum as an error, we need to implement the std:error::Error trait for it. As we know from the last article, the Error trait itself doesn’t need any extra implementation, but we need to implement Debug and Display.

Debug is easy to derive…

#[derive(Debug)]
enum NumFromFileErr {
ParseError(ParseIntError),
IoError(std::io::Error),
}

And Display is mainly writing the error messages of each of our errors into a formatter.

impl Display for NumFromFileErr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
NumFromFileErr::ParseError(parse_int_error) =>
write!(f, "{}", parse_int_error),
NumFromFileErr::IoError(io_error) =>
write!(f, "{}", io_error),
}
}
}

// Make it an error!
impl std::error::Error for NumFromFileErr {}

You can already sense the repetition coming. If our function might return a third error type, the NumFromFileErr enum, as well as the Display implementation, need adaption.

What about propagation? #

With that, we can already use our custom error in a Result<T, E>. If we change it (like in the following example on the first line), we get a couple of errors, though.

fn read_number_from_file(filename: &str) -> Result<u64, NumFromFileErr> {
let mut file = File::open(filename)?; // Error!

let mut buffer = String::new();

file.read_to_string(&mut buffer)?; // Error

let parsed: u64 = buffer.trim().parse()?; // Error

Ok(parsed)
}

What’s happening? The three methods in read_number_from_file still cause std::io::Error and std::num::ParseIntError. When we propagate them using the question mark operator ?, they are not compatible to NumFromFileErr. The Rust compiler tells us exactly what’s wrong (this one is to scroll):

error[E0277]: `?` couldn't convert the error to `NumFromFileErr`
--> src/main.rs:34:40
|
33 | fn read_number_from_file(filename: &str) -> Result<u64, NumFromFileErr> {
| --------------------------- expected `NumFromFileErr` because of this
34 | let mut file = File::open(filename)?;
| ^ the trait `From` is not implemented for `NumFromFileErr`

Let’s focus on the first line. The question mark operator couldn’t convert the error to NumberFromFileError. So let’s do that on our own. Match each error, if the operation was successful, return the value, if not, return with an Error from NumFromFileError

fn read_number_from_file(filename: &str) -> Result<u64, NumFromFileErr> {
let mut file = match File::open(filename) {
Ok(file) => file,
Err(err) => return Err(NumFromFileErr::IoError(err)), // 👀
};

let mut buffer = String::new();

match file.read_to_string(&mut buffer) {
Ok(_) => {}
Err(err) => return Err(NumFromFileErr::IoError(err)), // 👀
};

let parsed: u64 = match buffer.trim().parse() {
Ok(parsed) => parsed,
Err(err) => return Err(NumFromFileErr::ParseError(err)), // 👀
};

Ok(parsed)
}

Wow, that’s tedious! What happened to our sweet propagation? Well, the errors are incompatible, so we have to make them compatible. But there’s a better way to it. One that’s more idiomatic and is hinted at in the second part of the error message. the trait From<std::io::Error> is not implemented for NumFromFileErr

The From trait #

The From trait allows you to define how to go from one type to another. It’s a generic trait, where you specify which type you want to convert, and then implement it for your own types. Since we already defined how to treat ParseIntError and std::io::Error in the enum itself, the conversion implementations are pretty straightforward.

impl From<ParseIntError> for NumFromFileErr {
fn from(err: ParseIntError) -> Self {
NumFromFileErr::ParseError(err)
}
}

impl From<std::io::Error> for NumFromFileErr {
fn from(err: std::io::Error) -> Self {
NumFromFileErr::IoError(err)
}
}

Oh… can you smell the beauty of repetition? There is another way of converting one type into the other, by implementing the Into trait. If you need to implement the conversion, always go for From. The reverse Into trait comes along for free, due to this beauty in Rust’s core library:

impl<T, U> Into<U> for T
where
U: From<T>,
{
fn into(self) -> U {
U::from(self)
}
}

This implements conversion of Into for generic T, where we want to convert T into U. If U implements From<T> as defined by the trait boundary, we just call the respective from method. It’s beauties like these that make Rust such an elegant language and shows the true power of traits.

And that’s pretty much it. With the conversion to go from the two errors into our custom-defined one, error propagation works again!

fn read_number_from_file(filename: &str) -> Result<u64, NumFromFileErr> {
let mut file = File::open(filename)?;

let mut buffer = String::new();

file.read_to_string(&mut buffer)?;

let parsed: u64 = buffer.trim().parse()?;

Ok(parsed)
}

Sweet! A bit of extra boilerplate, but no trait objects. Nothing on the heap. No vtable for dynamic lookup. Much less runtime code. And some extra benefits…

Matchin enum branches vs downcasting #

One thing that really bugged me is downcasting from a trait object to a real struct. To me, this feels a lot like working with hot coals, because you never know which errors can actually occur. I think it’s guesswork if it’s not well documented. This here:

fn main() {
match read_number_from_file("number.txt") {
Ok(v) => println!("Your number is {}", v),
Err(err) => {
if let Some(io_err) = err.downcast_ref::<std::io::Error>() {
eprintln!("Error during IO! {}", io_err)
} else if let Some(pars_err) = err.downcast_ref::<ParseFloatError>() {
eprintln!("Error during parsing {}", pars_err)
}
}
};
}

perfectly compiles, even though my function never returns an error result of ParseFloatError. If we use enums, tooling and the enum itself tells us which possible errors are available. Also, working with those errors becomes very elegant again:

fn main() {
match read_number_from_file("number.txt") {
Ok(v) => println!("Your number is {}", v),
Err(err) => match err {
NumFromFileErr::IoError(_) => println!("Error from IO!"),
NumFromFileErr::ParseError(_) => println!("Error from Parsing!"),
},
};
}

This is also one of the beautiful things about Rust. It’s a language that allows you to go from a very low-level to a very high-level programming style without sacrificing elegance!

Repetition #

The only thing we sacrifice in comparison to Box<dyn Error>is the amount of boilerplate we need to create. Trait objects are just so convenient, aren’t they? But with everything that looks like repetition and boilerplate, also looks like we could have macros helping us with code generation. And with Rust, you can be pretty sure somebody already did that.

One crate that I found is thiserror, which helps you avoid repetition and allows for very complex custom error cases.

It might also be a fun exercise to create something like that on my own!

Bottom line #

Boxed trait objects have their purpose and are a really good way for handling cases that are only known at runtime. Box<dyn Error> is also something that looks like it’s very common. However, even though the enum version creates a lot more code, it feels also a lot less complicated to me. Enums are much simpler to handle than trait objects. How they affect memory is known at compile time. And an enum tells me exactly what my options are.

Whenever I run into functions that can propagate various errors, Enums as errors is my go-to way of handling them.

There’s also the perspective from David Tolnay, who created both thiserror and anyhow: Use thiserror if you care about designing your own dedicated error type(s) so that the caller receives exactly the information that you choose in the event of failure. This most often applies to library-like code. Use Anyhow if you don’t care what error type your functions return, you just want it to be easy. This is common in application-like code.

And, as always, there’s a link to the playground.

Related Articles