Tasks
Related Issues
PRs
thiserror usages
Status is based on current workspace scan (done = no thiserror::Error derives and no thiserror dep in Cargo.toml).
Intermediate plan
IMPORTANT: Before batch 6, review and merge #2375. Otherwise it will probably conflict. Besides that, it has been waiting long enough.
Details
Plan: Migrate all crates from thiserror to gix-error
Context
The gitoxide project is migrating error handling from thiserror enum-based errors to gix-error's
Exn-based approach. This plan provides the dependency-ordered migration sequence so crates can be
ported leaf-first without breaking intermediate builds. The number after each crate name is the
approximate count of thiserror-derived error types/variants to migrate.
gix-archive (1) and gix-worktree-stream (1) are already migrated and not listed.
Migration patterns
Reference: gix-error/src/lib.rs module docs, .github/copilot-instructions.md
- Replace
thiserror dep with gix-error in Cargo.toml
pub type Error = gix_error::Exn<gix_error::Message>; (or appropriate specific type)
#[error("...")] variants become message("...") / message!("...")
#[from]/#[source] variants become .or_raise(|| message("context"))?
- Callback bounds use bare
Exn, function returns use specific types like Exn<Message>
- At
gix boundary: convert to gix_error::Error (implements std::error::Error)
- Tests: pattern matches on variants become string assertions
Dependency-ordered migration batches
Crates within a batch can be migrated in parallel (no interdependencies).
Each batch depends only on batches above it being complete.
Batch 1 — Leaves (no deps on other migration-list crates)
| Crate |
Count |
Notes |
| gix-hash |
7 |
foundational, used everywhere |
| gix-url |
3 |
|
| gix-packetline |
3 |
|
| gix-features |
3 |
|
| gix-path |
2 |
|
| gix-attributes |
2 |
|
| gix-quote |
1 |
|
| gix-lock |
1 |
|
| gix-fs |
1 |
|
| gix-bitmap |
1 |
|
| gix-mailmap |
1 |
|
Batch 2
| Crate |
Count |
Depends on (migration-list only) |
| gix-object |
12 |
gix-hash |
| gix-config-value |
2 |
gix-path |
| gix-shallow |
2 |
gix-hash, gix-lock |
| gix-refspec |
1 |
gix-hash |
Batch 3
| Crate |
Count |
Depends on |
| gix-ref |
22 |
gix-hash, gix-object |
| gix-filter |
18 |
gix-object, gix-quote, gix-packetline, gix-attributes |
| gix-revwalk |
4 |
gix-hash, gix-object |
| gix-pathspec |
3 |
gix-attributes, gix-config-value |
| gix-prompt |
1 |
gix-config-value |
Batch 4
| Crate |
Count |
Depends on |
| gix-traverse |
3 |
gix-hash, gix-object, gix-revwalk |
| gix-config |
11 |
gix-config-value, gix-ref |
| gix-credentials |
6 |
gix-url, gix-prompt, gix-config-value |
| gix-discover |
4 |
gix-ref |
Batch 5
| Crate |
Count |
Depends on |
| gix-index |
11 |
gix-hash, gix-bitmap, gix-object, gix-traverse |
| gix-transport |
10 |
gix-url, gix-packetline, gix-credentials, gix-quote |
| gix-worktree-stream |
-- |
already migrated |
| gix-submodule |
6 |
gix-pathspec, gix-refspec, gix-config, gix-url |
Batch 6
| Crate |
Count |
Depends on |
| gix-diff |
8 |
gix-hash, gix-object, gix-filter, gix-index, gix-pathspec, gix-attributes, gix-traverse |
| gix-protocol |
9 |
gix-transport, gix-hash, gix-shallow, gix-ref, gix-credentials, gix-object, gix-revwalk, gix-lock, gix-refspec |
| gix-dir |
1 |
gix-index, gix-discover, gix-pathspec, gix-object |
| gix-worktree-state |
1 |
gix-object, gix-index, gix-filter |
| gix-archive |
-- |
already migrated |
Batch 7
| Crate |
Count |
Depends on |
| gix-pack |
23 |
gix-object, gix-traverse, gix-diff, gix-hash |
| gix-merge |
8 |
gix-hash, gix-object, gix-filter, gix-diff, gix-revwalk, gix-index |
| gix-status |
2 |
gix-index, gix-hash, gix-object, gix-filter, gix-pathspec, gix-dir, gix-diff |
| gix-blame |
1 |
gix-revwalk, gix-diff, gix-object, gix-hash, gix-traverse |
Batch 8
| Crate |
Count |
Depends on |
| gix-odb |
11 |
gix-hash, gix-object, gix-pack |
Batch 9 — Top-level
| Crate |
Count |
Notes |
| gix |
138 |
Porcelain crate. Uses gix::Error = gix_error::Error (implements std::error::Error). Largest migration, likely needs multiple sessions. |
Per-crate migration checklist
For each crate:
Cargo.toml: replace thiserror with gix-error
- Find all
#[derive(thiserror::Error)] enums
- Replace with
pub type Error = Exn<Message> (or appropriate type)
- Convert each variant per the patterns in
gix-error/src/lib.rs "Migrating from thiserror"
- Update call sites:
Err(Error::Variant) → Err(message("...").raise())
- Update
#[from]/#[source] call sites to use .or_raise(|| message("..."))?
- Update tests: enum matching → string assertions
cargo check -p <crate> and cargo test -p <crate>
- Check downstream crates still compile (especially the next batch)
Verification
After each batch:
cargo check -p <crate> for each migrated crate
cargo test -p <crate> for each migrated crate
cargo check -p gix to catch downstream breakage early
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.
Tasks
anyhowinteraction (to keep the source-chain alive)cargo nextest --workflowrun without--exclude gix-errorby having multiple expectations there, depending on the set feature toggles.thiserrorwithgix-erroreverywhereNotARepositoryand something went wrong opening itgix_error::Errorwhen it helps with exn errors.'gix-validateasgix-error::ValidationError(currently it's an enum)Related Issues
PRs
gix-errorpunch-through #2352gix-error#2373gix-commitgraphtogix-error#2378anyhowintegration forgix-error#2383thiserrorwith a custom implemenation of error types. #2389gix-errorconversions: gix-actor #2396gix-error#2400thiserrorusagesStatus is based on current workspace scan (
done= nothiserror::Errorderives and nothiserrordep inCargo.toml).gix- count: 138gix-pack- count: 23gix-ref- count: 22gix-filter- count: 18gix-object- count: 12gix-config- count: 11gix-index- count: 11gix-odb- count: 11gix-transport- count: 10gix-protocol- count: 9gix-diff- count: 8gix-merge- count: 8gix-hash- count: 7gix-credentials- count: 6gix-submodule- count: 6gix-discover- count: 4gix-revwalk- count: 4gix-features- count: 3gix-packetline- count: 3gix-pathspec- count: 3gix-traverse- count: 3gix-url- count: 3gix-attributes- count: 2gix-config-value- count: 2gix-path- count: 2gix-shallow- count: 2gix-status- count: 2 (current derives: 3)gix-archivegix-bitmapgix-blame- count: 1gix-dir- count: 1gix-fs- count: 1gix-lock- count: 1gix-mailmapgix-prompt- count: 1gix-quotegix-refspec- count: 1gix-worktree-state- count: 1gix-worktree-streamgix-actorgix-chunkgix-command(no error at all)gix-commitgraphgix-dategix-errorgix-fetchhead(no error at all)gix-fsck(no error at all)gix-glob(no error at all)gix-hashtable(no error at all)gix-ignore(no error at all)gix-lfs(no error at all)gix-macros(no error at all)gix-negotiate(no error at all)gix-note(no error at all)gix-rebase(no error at all)gix-revisiongix-sec(no error at all)gix-sequencer(no error at all)gix-tempfile(no error at all)gix-tix(no error at all)gix-trace(no error at all)gix-tui(no error at all)gix-utils(manual error: 1)gix-validate(manual error: 4)gix-worktree(no error at all)Intermediate plan
IMPORTANT: Before batch 6, review and merge #2375. Otherwise it will probably conflict. Besides that, it has been waiting long enough.
Details
Plan: Migrate all crates from
thiserrortogix-errorContext
The gitoxide project is migrating error handling from
thiserrorenum-based errors togix-error'sExn-based approach. This plan provides the dependency-ordered migration sequence so crates can beported leaf-first without breaking intermediate builds. The number after each crate name is the
approximate count of
thiserror-derived error types/variants to migrate.gix-archive(1) andgix-worktree-stream(1) are already migrated and not listed.Migration patterns
Reference:
gix-error/src/lib.rsmodule docs,.github/copilot-instructions.mdthiserrordep withgix-errorinCargo.tomlpub type Error = gix_error::Exn<gix_error::Message>;(or appropriate specific type)#[error("...")]variants becomemessage("...")/message!("...")#[from]/#[source]variants become.or_raise(|| message("context"))?Exn, function returns use specific types likeExn<Message>gixboundary: convert togix_error::Error(implementsstd::error::Error)Dependency-ordered migration batches
Crates within a batch can be migrated in parallel (no interdependencies).
Each batch depends only on batches above it being complete.
Batch 1 — Leaves (no deps on other migration-list crates)
Batch 2
Batch 3
Batch 4
Batch 5
Batch 6
Batch 7
Batch 8
Batch 9 — Top-level
gix::Error = gix_error::Error(implementsstd::error::Error). Largest migration, likely needs multiple sessions.Per-crate migration checklist
For each crate:
Cargo.toml: replacethiserrorwithgix-error#[derive(thiserror::Error)]enumspub type Error = Exn<Message>(or appropriate type)gix-error/src/lib.rs"Migrating from thiserror"Err(Error::Variant)→Err(message("...").raise())#[from]/#[source]call sites to use.or_raise(|| message("..."))?cargo check -p <crate>andcargo test -p <crate>Verification
After each batch:
cargo check -p <crate>for each migrated cratecargo test -p <crate>for each migrated cratecargo check -p gixto catch downstream breakage earlyGenAI Notes
Refine this prompt for better results, going one crate at a time.
Actually, genAI isn't good at this, it just doesn't get it and creates a convoluted mess.
What is can do is turn
thiserrorinto 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
exncrate compared tothiserror/anyhowThe benefits of
exn:anyhowhas 14k)thiserrorhas 3 or 4 heavy ones)it doesn't leak out of the crate, error types are hand-implemented structs or enumsexn::Resultwhich hides theResult<(), Exn<ErrorType>>Disadvantages compared to
thiserrorExntype is exposed in the typesystem, it wraps the actual type.exn::Result, and is very common also foranyhow::Resultin 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 marryinggix-withexnin that way.gix-errorseven, and maybe that is what should be done to gain a little distance.Benefits of the
exnerror handling styleOf course, the presentation of errors, can be adjusted, but this is completely controlled by the calling application, and
gixcould provide its own application errors as utility if it wanted to (probably not).Basic Example
Needed in
exnThings I noticed when porting
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)" })))))))anyhow, so that error chains work correctly.gix-errorwould have to have a feature (default on viagix) to auto-setup an error chain and completely dissolveExn.into-anyhowfeature that uses publicly accessible methods to convert into an anyhow::Error.iter_chain()orsources(). Note thatsource()has been converted intoSourceErrorErrormust not loose call locations, try to keep them by using frames.