chore: Better error types for TreeBuilder #563

Merged
wetneb merged 5 commits from 552-error-types into main 2025-10-03 10:40:52 +02:00
Owner

This is an attempt to start revamping our error handling (for #552).

I read up a little about the best practices for error handling in Rust, but I'm sure there is still plenty to improve.

It also turns the panic of #556 into a recoverable error, meaning that we fall back on line-based merging in such a case. Similarly, I've changed other explicit panics to recoverable errors. It would still be good to panic on them in development mode and I'm not sure what's the cleanest way to do that. If I add a panic! gated by #[config(debug_assertions)], rustc complains that the following return statement is unreachable (while it is reachable in release mode). Ideally, the panicking code would not be inserted at every callsite where one such error is raised, but only once, at the definition of the error variant… if it had a sort of constructor, then the panicking could be done there.

This is an attempt to start revamping our error handling (for #552). I read up a little about the best practices for error handling in Rust, but I'm sure there is still plenty to improve. It also turns the panic of #556 into a recoverable error, meaning that we fall back on line-based merging in such a case. Similarly, I've changed other explicit panics to recoverable errors. It would still be good to panic on them in development mode and I'm not sure what's the cleanest way to do that. If I add a `panic!` gated by `#[config(debug_assertions)]`, rustc complains that the following return statement is unreachable (while it is reachable in release mode). Ideally, the panicking code would not be inserted at every callsite where one such error is raised, but only once, at the definition of the error variant… if it had a sort of constructor, then the panicking could be done there.
chore: Better error types for TreeBuilder
All checks were successful
/ test (pull_request) Successful in 1m56s
8197b2087a
Owner

I read up a little about the best practices for error handling in Rust

Since you mentioned it, here's a nice blog post I recently saw on this topic.

if it had a sort of constructor, then the panicking could be done there

yes; for that, you could have a struct for that particular error variant, put the panicking logic into its constructor as you say, and impl From<ThatErrorStruct> for TreeBuildingError so that you could do the following in the place you create and then return it with a ?:

if something_bad_happened() {
    Err(ThatErrorStruct::new(param))?;
}

But I don't really think we'll be throwing the same error from different places all that often.. so I'd go with a cfg!-d panic! approach, and I think you can avoid unreachable_code warnings by doing the following:

if something_bad_happened() {
    if cfg!(debug_assertions) {
        panic!("some message")
    } else {
       Err(ThatErrorStruct::new(param))?;
    }
}

It's of course unfortunate that that'll require us to repeat the error message (once in the #[error()] of the enum variant and once in the panic!), so maybe we could something like this instead?

if something_bad_happened() {
    let err = Err(ThatErrorStruct::new(param).into());
    if cfg!(debug_assertions) {
        err.unwrap();
    } else {
       return err;
    }
}

Not saying that this is pretty of course...

> I read up a little about the best practices for error handling in Rust Since you mentioned it, here's a nice [blog post](https://sabrinajewson.org/blog/errors#guidelines-for-good-errors) I recently saw on this topic. > if it had a sort of constructor, then the panicking could be done there yes; for that, you could have a struct for that particular error variant, put the panicking logic into its constructor as you say, and `impl From<ThatErrorStruct> for TreeBuildingError` so that you could do the following in the place you create and then return it with a `?`: ```rs if something_bad_happened() { Err(ThatErrorStruct::new(param))?; } ``` But I don't really think we'll be throwing the same error from different places all that often.. so I'd go with a `cfg!`-d `panic!` approach, and I think you can avoid `unreachable_code` warnings by doing the following: ```rs if something_bad_happened() { if cfg!(debug_assertions) { panic!("some message") } else { Err(ThatErrorStruct::new(param))?; } } ``` It's of course unfortunate that that'll require us to repeat the error message (once in the `#[error()]` of the enum variant and once in the `panic!`), so maybe we could something like this instead? ```rs if something_bad_happened() { let err = Err(ThatErrorStruct::new(param).into()); if cfg!(debug_assertions) { err.unwrap(); } else { return err; } } ``` Not saying that this is pretty of course...
Author
Owner

Since you mentioned it, here's a nice blog post I recently saw on this topic.

That's indeed one that I enjoyed reading! I didn't go for the recommended solution of having an enum in a struct, because I felt this was mostly motivated by API stability considerations which don't apply to us as we don't intend to maintain a stable Rust API (so far).

so I'd go with a cfg!-d panic! approach

Makes sense! I didn't know about cfg!, it's a good solution.

> Since you mentioned it, here's a nice blog post I recently saw on this topic. That's indeed one that I enjoyed reading! I didn't go for the recommended solution of having an enum in a struct, because I felt this was mostly motivated by API stability considerations which don't apply to us as we don't intend to maintain a stable Rust API (so far). > so I'd go with a `cfg!`-d `panic!` approach Makes sense! I didn't know about `cfg!`, it's a good solution.
Add back panics in debug mode
All checks were successful
/ test (pull_request) Successful in 1m51s
5c7a18b062
ada4a left a comment
Owner

oops, apparently forgot to send those out a while ago

oops, apparently forgot to send those out a while ago
@ -69,0 +84,4 @@
NoVirtualRootChildFound,
#[error("impossible to do a line-based fallback merge for a virtual node")]
LineBasedFallbackOnVirtualNode,
#[error("build_conflict returned something else than a conflict")]
Owner

build_conflict lost its surrounding backticks during the move it seems..

`build_conflict` lost its surrounding backticks during the move it seems..
wetneb marked this conversation as resolved
@ -350,2 +375,4 @@
.next()
.ok_or(TreeBuildingError::NoVirtualRootChildFound),
PCSNode::LeftMarker => {
panic!("impossible to build a subtree for a virtual left marker")
Owner

this panic! and the next could be turned into an error as well I guess?

this `panic!` and the next could be turned into an error as well I guess?
Owner

This comment might've flown under the radar^^

This comment might've flown under the radar^^
src/utils.rs Outdated
@ -0,0 +1,18 @@
use std::error::Error;
pub(crate) trait InternalError<V, E: Error> {
Owner

I think you don't actually need the generics on the trait itself, since you only use them in the impl for Result? (the impl can have generics itself)

I think you don't actually need the generics on the trait itself, since you only use them in the impl for `Result`? (the impl can have generics itself)
Author
Owner

Yes and I forgot to git add other changes - let's pretend it's the fault of my train's wifi connection!

Yes and I forgot to `git add` other changes - let's pretend it's the fault of my train's wifi connection!
ada4a marked this conversation as resolved
wetneb force-pushed 552-error-types from 5c7a18b062 to c407028ca9 2025-08-14 17:04:01 +02:00 Compare
Owner

That's indeed one that I enjoyed reading!

I guess we're following the same people on Mastodon:)

I didn't go for the recommended solution of having an enum in a struct, because I felt this was mostly motivated by API stability considerations which don't apply to us as we don't intend to maintain a stable Rust API

Yup, fair enough

> That's indeed one that I enjoyed reading! I guess we're following the same people on Mastodon:) > I didn't go for the recommended solution of having an enum in a struct, because I felt this was mostly motivated by API stability considerations which don't apply to us as we don't intend to maintain a stable Rust API Yup, fair enough
wetneb changed title from chore: Better error types for TreeBuilder to WIP: chore: Better error types for TreeBuilder 2025-08-17 09:50:28 +02:00
wetneb force-pushed 552-error-types from c407028ca9 to b08cda9a6a 2025-09-30 19:36:22 +02:00 Compare
wetneb changed title from WIP: chore: Better error types for TreeBuilder to chore: Better error types for TreeBuilder 2025-09-30 19:38:14 +02:00
ada4a requested changes 2025-10-01 14:55:03 +02:00
Dismissed
@ -0,0 +9,4 @@
/// This is useful for internal errors that are meant to be never reached,
/// but that we want to be able to gracefully recover from in release mode.
#[inline]
fn debug_panic(self) -> Self {
Owner

I think this would benefit from #[track_caller], which would make it so that the reported panic location is at the caller of this function and not inside this function

I think this would benefit from `#[track_caller]`, which would make it so that the reported panic location is at the caller of this function and not inside this function
ada4a marked this conversation as resolved
Add track_caller
All checks were successful
/ test (pull_request) Successful in 37s
906fcc39ae
Convert more panics
All checks were successful
/ test (pull_request) Successful in 37s
8be9512c0a
@ -279,0 +298,4 @@
_ => {
return Err(TreeBuildingError::MoreThanTwoConflictingSides {
node: predecessor,
});
Owner

Correct me if I'm wrong, but isn't this case exactly as unexpected as the "0" case, and if so, shouldn't it be handled in the same way? So if debug assertions are enabled, we would just panic, and otherwise.. I'm not sure whether we should do commutative_or_line_based_local_fallback or just return an Err

Correct me if I'm wrong, but isn't this case exactly as unexpected as the "0" case, and if so, shouldn't it be handled in the same way? So if debug assertions are enabled, we would just panic, and otherwise.. I'm not sure whether we should do `commutative_or_line_based_local_fallback` or just return an `Err`
Author
Owner

I couldn't remember it so I just added a panic!() there to see when it is reached. Apparently it is reached in delete/delete or delete/modified conflicts, and this fallback to line-based merging is expected, so we shouldn't debug_panic here.

I couldn't remember it so I just added a `panic!()` there to see when it is reached. Apparently it is reached in delete/delete or delete/modified conflicts, and this fallback to line-based merging is expected, so we shouldn't `debug_panic` here.
Add note for the 0 successors case
All checks were successful
/ test (pull_request) Successful in 44s
80c6ce352b
ada4a approved these changes 2025-10-03 10:27:52 +02:00
ada4a left a comment
Owner

Very nice, thank you:)

Very nice, thank you:)
wetneb merged commit 9506d8aa17 into main 2025-10-03 10:40:52 +02:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: mergiraf/mergiraf#563
No description provided.