Skip to content

Reorganise Error-Handling around Exn #2351

@Byron

Description

@Byron

Tasks

  • Proof of concept
  • anyhow interaction (to keep the source-chain alive)
    • make cargo nextest --workflow run without --exclude gix-error by having multiple expectations there, depending on the set feature toggles.
  • replace thiserror with gix-error everywhere
  • Make it easy to differentiate between NotARepository and something went wrong opening it

Related Issues

PRs

thiserror usages

 138 gix
  23 gix-pack
  22 gix-ref
  18 gix-filter
  12 gix-object
  11 gix-odb
  11 gix-index
  11 gix-config
  10 gix-transport
   9 gix-protocol
   8 gix-merge
   8 gix-diff
   7 gix-hash
   6 gix-submodule
   6 gix-credentials
   4 gix-revwalk
   4 gix-discover
   3 gix-url
   3 gix-traverse
   3 gix-pathspec
   3 gix-packetline
   3 gix-features
   2 gix-status
   2 gix-shallow
   2 gix-path
   2 gix-config-value
   2 gix-attributes
   1 gix-worktree-stream
   1 gix-worktree-state
   1 gix-refspec
   1 gix-quote
   1 gix-prompt
   1 gix-mailmap
   1 gix-lock
   1 gix-fs
   1 gix-dir
   1 gix-blame
   1 gix-bitmap
   1 gix-archive

GenAI Notes

Refine this prompt for better results, going one crate at a time.

In the CRATENAME, replace thiserror with gix-error after reading the documentation of gix-error/src/lib.rs carefully to know how to use gix-error correctly.

Actually, genAI isn't good at this, it just doesn't get it and creates a convoluted mess.
What is can do is turn thiserror into the manual implementation, but that's not super useful to start with, and I'd argue that one can do this better by hand.
Fair enough, it's my daily night task.

Benefits of the exn crate compared to thiserror/anyhow

The benefits of exn:

  • it's small at ~300 SLOC (anyhow has 14k)
  • it doesn't use proc-macros and has 0 dependencies (thiserror has 3 or 4 heavy ones)
  • it doesn't leak out of the crate, error types are hand-implemented structs or enums
    • Actually it does leak out, as the examples show an exn::Result which hides the Result<(), Exn<ErrorType>>
  • Call locations by default, without overhead or full backtraces

Disadvantages compared to thiserror

  • The Exn type is exposed in the typesystem, it wraps the actual type.
    • This can be hidden with exn::Result, and is very common also for anyhow::Result in applications. It's not common in plumbing crates, but I feel strongly that hiding it will be enough, with benefits clearly outweighing the disadvantage of marrying gix- with exn in that way.
    • If that's ever a problem, it can be moved into gix-errors even, and maybe that is what should be done to gain a little distance.

Benefits of the exn error handling style

  • sources of errors are gathered automatically
  • error chains/trees are searchable by downcasting
  • errors are organised by their value for the caller, and not by what went wrong

Of course, the presentation of errors, can be adjusted, but this is completely controlled by the calling application, and gix could provide its own application errors as utility if it wanted to (probably not).

Basic Example
// Copyright 2025 FastLabs Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! # Basic Example - Error Handling Best Practices
//!
//! This example demonstrates the recommended patterns for using `exn`:
//!
//! 1. **Define Error Types Per Module** - Each module has its own error type. The type system
//!    enforces proper error context via `or_raise()`.
//!
//! 2. **Don't Chain Errors Manually** - Unlike traditional error handling, you don't need `source:
//!    Box<dyn Error>` in your types. The `exn` framework maintains the error chain automatically.
//!
//! 3. **Keep Errors Simple** - Use `struct Error(String)` by default. Only add complexity (enums,
//!    fields) when needed for programmatic handling.

use derive_more::Display;
use exn::Result;
use exn::ResultExt;
use exn::bail;

fn main() -> Result<(), MainError> {
    app::run().or_raise(|| MainError)?;
    Ok(())
}

#[derive(Debug, Display)]
#[display("fatal error occurred in application")]
struct MainError;
impl std::error::Error for MainError {}

mod app {
    use super::*;

    pub fn run() -> Result<(), AppError> {
        // When crossing module boundaries, use or_raise() to add context
        http::send_request("https://example.com")
            .or_raise(|| AppError("failed to run app".to_string()))?;
        Ok(())
    }

    #[derive(Debug, Display)]
    pub struct AppError(String);
    impl std::error::Error for AppError {}
}

mod http {
    use super::*;

    pub fn send_request(url: &str) -> Result<(), HttpError> {
        std::fs::File::open("does not exist")
            .or_raise(|| HttpError(format!("Failed to open {url}")))
            .map(|_| ())
    }

    #[derive(Debug, Display)]
    pub struct HttpError(String);
    impl std::error::Error for HttpError {}
}

// Error: fatal error occurred in application, at examples/src/basic.rs:34:16
// |
// |-> failed to run app, at examples/src/basic.rs:49:14
// |
// |-> Failed to open https://example.com, at examples/src/basic.rs:63:14
// |
// |-> No such file or directory (os error 2), at examples/src/basic.rs:63:1

Needed in exn

Things I noticed when porting

  • Good debug printing so we get something akin to failed to create index: Git(FetchDuringClone(PrepareFetch(RefMap(Handshake(Transport(Io(Custom { kind: Other, error: "error sending request for url (https://github.com/rust-lang/crates.io-index/info/refs?service=git-upload-pack)" })))))))
    • Be sure this contains call locations, allowing people to help themselves more easily.
  • Validate that interop with anyhow, so that error chains work correctly.
    • Actually, gix-error would have to have a feature (default on via gix) to auto-setup an error chain and completely dissolve Exn.
    • Chained parts should always be linked-lists, until they can't be, or the 'chain' feature is set.
    • Actually, let's do it with an into-anyhow feature that uses publicly accessible methods to convert into an anyhow::Error.
  • Error iterator similar to iter_chain() or sources(). Note that source() has been converted into SourceError
  • Error must not loose call locations, try to keep them by using frames.

Metadata

Metadata

Assignees

Labels

C-tracking-issueAn issue to track to track the progress of multiple PRs or issues

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions