Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,18 @@ jobs:
# the tests gated on those features only run here.
- name: Test all features
run: cargo test --workspace --all-features
# With default features `error-forge` must not pull any
# third-party crate into a dependent's build, on any target.
# `error-forge-derive` is in-house and dependency-free without
# its `derive` feature.
- name: Default dependency graph has no third-party crates
run: |
deps=$(cargo tree -p error-forge -e normal --target all --prefix none --locked | sort -u | grep -v -E '^error-forge(-derive)? v' || true)
if [ -n "$deps" ]; then
echo "Third-party crates in the default build of error-forge:"
echo "$deps"
exit 1
fi

docs:
runs-on: ubuntu-latest
Expand Down
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

Minor release for `error-forge` and `error-forge-derive` (both `1.1.0`). With default features, `error-forge` no longer brings any third-party crate into a dependent's build: `cargo tree -e normal` lists 8 third-party crates for `1.0.2` on Windows, Linux and macOS and none for `1.1.0`. No public item is removed, renamed or changed, and default features stay `[]`.

### Added

- `error-forge-derive` has a `derive` feature, on by default, that gates `#[derive(ModError)]` and the `syn`, `quote` and `proc-macro2` dependencies. Without it the crate has no dependencies.
- `error-forge` declares its `error-forge-derive` feature explicitly. Cargo created it implicitly while `error-forge-derive` was an optional dependency; it keeps its meaning (it compiles the derive macro, and `derive` implies it), so manifests that name it keep resolving.
- CI checks that the default `error-forge` dependency graph contains no third-party crate on any target.

### Changed

- `define_errors!` builds its constructor names (`fn [<Variant:lower>]`) with an internal identifier-pasting macro in `error-forge-derive` instead of the third-party `pastey` crate. Constructor names, spans and error messages for invalid names (a variant such as `Type` whose lowercase name is a keyword) are unchanged. The macro is reachable as `error_forge::__private::paste!`, which is not public API; the hidden `error_forge::__private::pastey` re-export is gone.
- `error-forge-derive` is now an always-on dependency of `error-forge`, with its default features off. The `derive` feature enables `error-forge-derive/derive` through the `error-forge-derive` feature.
- `CircuitBreaker` uses `std::sync::Mutex` instead of `parking_lot::Mutex`. Every acquisition goes through `PoisonError::into_inner`, so no method can panic on a poisoned lock, as before. `CircuitBreaker` is now `UnwindSafe` and `RefUnwindSafe`.
- `CircuitBreaker::execute` takes its lock once instead of twice for a successful call in the closed state; only the half-open probe takes it again. Against `1.0.2` (release build, no-op closure): uncontended `execute` is 17% faster on Windows and 20% faster on Linux (41.2 to 34.2 ns, 38.9 to 31.3 ns); eight threads sharing one breaker are 47% faster on Windows and 10% faster on Linux.
- A clean debug build of a crate that depends on `error-forge` with default features takes 0.78 s instead of 1.09 s on 32 jobs and 0.99 s instead of 2.67 s on one job (median of five, Windows, `CARGO_INCREMENTAL=0`).
- `docs/STABILITY.md` lists the new dependency set and narrows the dependency rules to what dependents can observe: adding a third-party dependency outside an opt-in feature is still a `2.0.0` change; removing one whose types are not in the public API is not. `docs/architecture.md`, `docs/COMPARISON.md`, `docs/migration.md`, `SECURITY.md` and the README describe the new dependencies.
- `error-forge-derive`'s `#[derive(ModError)]` implementation moved from `src/lib.rs` to `src/mod_error.rs`. No behaviour change.

### Removed

- The `pastey` and `parking_lot` dependencies, and with them `lock_api`, `parking_lot_core`, `scopeguard`, `smallvec`, `cfg-if` and `windows-link` (Windows) or `libc` (Unix) from every dependent's build. None of their types appear in the public API.

## [1.0.2] - 2026-10-08

Patch release for `error-forge` and `error-forge-derive` (both `1.0.2`). Bug fixes, hygiene and build-time improvements; no public item added, removed or renamed (`cargo semver-checks` against `1.0.1`: 222 checks pass). Some inputs that compiled but silently produced wrong output are now compile errors; see **Changed**.
Expand Down
12 changes: 2 additions & 10 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 13 additions & 14 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ name = "error-forge"
# public types, `Result<T>` deprecated in favour of `AppResult<T>`,
# `register_error_hook` (non-try) deprecated, `rand` gated behind
# new `jitter` feature.
version = "1.0.2"
version = "1.1.0"
edition = "2021"
# MSRV. The crate uses `io::Error::other` (stabilised 1.74),
# `Mutex::new()` in const context (1.63 — fine), and Cargo lockfile
Expand Down Expand Up @@ -59,19 +59,13 @@ rustdoc-args = ["--cfg", "docsrs"]


[dependencies]
# `pastey` is a drop-in fork of `paste`. The original `paste` crate
# was archived by its author on 2024-10-07 (RUSTSEC-2024-0436,
# "unmaintained"); `pastey` keeps the public `paste!` macro API and
# behaviour, with the same `[<...>]` token-pasting and case-modifier
# syntax `define_errors!` relies on.
pastey = "0.2"
# `parking_lot` provides a non-poisoning `Mutex` used by
# `CircuitBreaker`'s internal state. The standard `std::sync::Mutex`
# poisons on panic, which is incompatible with this crate's
# error-handling premise; `parking_lot::Mutex` returns the guard
# directly with no `Result` to unwrap.
parking_lot = "0.12"
error-forge-derive = {version = "1.0.2", path = "./error-forge-derive", optional = true }
# The only always-on dependency, and an in-house one. With default
# features off it is a dependency-free proc-macro crate that supplies
# the identifier pasting `define_errors!` needs (it replaced the
# third-party `pastey`, and before that `paste`). The `derive` feature
# below turns on its `#[derive(ModError)]`, which brings in `syn`,
# `quote` and `proc-macro2`.
error-forge-derive = { version = "1.1.0", path = "./error-forge-derive", default-features = false }
# Floors below are checked by the `minimal-versions` CI job: `serde`
# 1.0.157 is the first release whose derive builds alongside the
# `syn` 1 floor of `error-forge-derive`, `log` 0.4.4 the first whose
Expand All @@ -91,6 +85,11 @@ rand = { version = "0.8.5", optional = true }
[features]
default = []
derive = ["error-forge-derive"]
# Up to 1.0.2 `error-forge-derive` was an optional dependency and this
# was the feature Cargo created for it. It is kept, with the same
# meaning, so manifests that name it keep resolving and `derive` still
# implies it: it compiles the derive macro, and `derive` re-exports it.
error-forge-derive = ["error-forge-derive/derive"]
serde = ["dep:serde"]
# `console`, `backtrace`, `registry`, `collector` and `context` gate
# nothing: the related APIs are always compiled in and no backtrace is
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ It ships with a built-in `AppError`, a declarative `define_errors!` macro, an op

```toml
[dependencies]
error-forge = "1.0.2"
error-forge = "1.1.0"
```

MSRV: Rust `1.81`. CI builds and tests the crate on the exact `1.81.0` toolchain.
Expand All @@ -43,6 +43,8 @@ Common optional features:
- `tracing`: enables the `tracing` adapter
- `jitter`: enables ±20% jitter in `ExponentialBackoff` (pulls in `rand`)

With default features, `error-forge` brings in no third-party crates: its only always-on dependency is its own `error-forge-derive`, which has no dependencies of its own unless the `derive` feature is on. The optional features above add their third-party crates (`syn`, `quote` and `proc-macro2` for `derive`).

The `console`, `backtrace`, `registry`, `collector` and `context` features exist but currently gate nothing: console formatting, the error-code registry, collectors and context wrapping are always compiled in, and no backtrace is captured automatically. They are kept so existing `Cargo.toml` files keep resolving; enabling them has no effect.

## Quick Start
Expand Down
9 changes: 5 additions & 4 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,14 @@ Security-relevant issues include but are not limited to:

- Memory safety bugs (use-after-free, double-free, OOB access)
— even though `error-forge` has no `unsafe` of its own, a bug
that triggers UB in `parking_lot`, `pastey`, or the standard
library through our usage qualifies.
that triggers UB in a dependency or the standard library
through our usage qualifies.
- Information leakage through panic payloads, error messages,
or `Display` / `Debug` impls.
- Denial of service via unbounded allocation, infinite loops,
or lock-poisoning cascades (the `CircuitBreaker` uses
`parking_lot::Mutex` specifically to avoid the latter).
or lock-poisoning cascades (the `CircuitBreaker` takes its
lock through `PoisonError::into_inner` specifically to avoid
the latter).
- Logic bugs in the `recovery` primitives that produce
incorrect retry / circuit-breaker behaviour under
adversarial input.
Expand Down
11 changes: 7 additions & 4 deletions docs/COMPARISON.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,9 @@ attributes. Zero runtime cost.
### Things `thiserror` does that `error-forge` doesn't

- **Pure compile-time.** No runtime dependency at all.
`error-forge` has runtime deps (`pastey`, `parking_lot`).
`error-forge` has one always-on dependency, its own
dependency-free `error-forge-derive`, compiled as a proc
macro.
- **Standard error-attribute syntax.** Every Rust dev knows
`#[error("...")]`.
- **Zero metadata.** Just `Error` + `Display`. Nothing more,
Expand Down Expand Up @@ -217,9 +219,10 @@ mirror the `anyhow` discussion above.
The cases where `error-forge` is clearly *not* the right
choice:

1. **You want zero runtime deps.** `error-forge` has two
always-on deps (`pastey`, `parking_lot`) and
optional deps behind opt-in features. `thiserror` alone has
1. **You want zero runtime deps.** `error-forge` has one
always-on dependency (its own `error-forge-derive`, with no
dependencies of its own unless `derive` is on) and optional
third-party deps behind opt-in features. `thiserror` alone has
zero runtime cost; choose it if dep footprint is the
constraint.

Expand Down
45 changes: 31 additions & 14 deletions docs/STABILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@ any of the following requires `2.0.0`:
- Changing the default-feature set
(default features are `[]` in `1.0.0`; adding a default
feature, or making a default feature opt-in, is breaking).
- Adding a runtime dependency that is not gated behind an
opt-in feature.
- Adding a third-party runtime dependency that is not gated
behind an opt-in feature.
- A change that breaks the documented `0.9.x → 1.0.0`
migration in [`docs/migration.md`](migration.md).

Expand Down Expand Up @@ -130,8 +130,10 @@ be a bug. Specifically:
`Err("Error hook already registered")` instead.
- `ConsoleTheme` formatting never panics; ANSI escapes are
static strings.
- `recovery::CircuitBreaker` uses `parking_lot::Mutex`, which
does not poison; `lock()` cannot panic in our usage.
- `recovery::CircuitBreaker` never panics on a poisoned lock: the
protected closure runs outside the lock, and the internal
`std::sync::Mutex` is taken through `PoisonError::into_inner`
(`parking_lot::Mutex` up to `1.0.2`).
- `recovery::RetryExecutor::retry` propagates the inner
operation's panic if the closure panics (matches what every
retry library does).
Expand All @@ -146,7 +148,7 @@ The stability contract does NOT cover:
`"[CODE] "` on `CodedError`-formatted output are part of the
contract; the human-readable text following them is not.
- **Internal types and modules.** Any item not re-exported
through `lib.rs` (`pastey`, internal helpers in
through `lib.rs` (`__private::paste!`, internal helpers in
`error.rs::panic_payload_to_listener_error`, the private
`recovery::retry::{BackoffStrategy, BackoffType, RetryPredicate}`)
is internal and may move or change between minor releases.
Expand All @@ -158,28 +160,43 @@ The stability contract does NOT cover:

The following runtime dependencies are sealed for the `1.x` line:

- `pastey` (always-on, macro support — drop-in fork of the
archived `paste`).
- `parking_lot` (always-on, non-poisoning `Mutex` used by
`recovery::CircuitBreaker`).
- `error-forge-derive` (optional, gated on `derive` feature).
- `error-forge-derive` (always-on, in-house, same version as
`error-forge`). With default features off it has no
dependencies and supplies the identifier pasting
`define_errors!` uses. Its `#[derive(ModError)]` and the `syn`,
`quote` and `proc-macro2` it needs are gated on the `derive`
feature.
- `serde` (optional, gated on `serde` feature).
- `log` (optional, gated on `log` feature).
- `tracing` (optional, gated on `tracing` feature).
- `async-trait` (optional, gated on `async` feature).
- `rand` (optional, gated on `jitter` feature).

Adding a new runtime dependency requires a `2.0.0` bump.
Removing any of the always-on deps requires a `2.0.0` bump.
Adding new optional dependencies behind a new opt-in feature is
a minor-version bump.
With default features the dependency graph contains no
third-party crate. Adding a third-party runtime dependency that is
not behind an opt-in feature requires a `2.0.0` bump. Adding
optional dependencies behind a new opt-in feature is a
minor-version bump. Removing a dependency whose types do not
appear in the public API is not a breaking change: Cargo does not
let a crate use a transitive dependency it does not declare.

`1.0.0` and `1.0.1` also listed `thiserror` here. No code in either
crate used it and no `thiserror` type appears in the public API, so
dependents could not observe it (Cargo does not let a crate use a
transitive dependency it does not declare). `1.0.2` drops it as a
build-time fix.

`1.0.x` also listed `pastey` and `parking_lot` as always-on and
said removing an always-on dependency required `2.0.0`. That rule
protected nothing: neither crate's types appear in the public API,
so no dependent could name or rely on them. `1.1.0` removes both
and narrows the rules above to what dependents can observe.
`define_errors!` now pastes identifiers through
`error-forge-derive` (previously an optional dependency, now
always on with its own dependencies switched off), and
`CircuitBreaker` uses `std::sync::Mutex`. The default dependency
graph went from eight third-party crates to none.

## MSRV

`1.0.0` ships with MSRV `1.81.0`. Any change to the MSRV is a
Expand Down
23 changes: 20 additions & 3 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,16 +126,33 @@ behaviour with their async runtime's retry helper.
The blocking sleep is fine for worker-thread code, batch jobs,
and CLI tools — all of which are valid `error-forge` use cases.

## `CircuitBreaker` uses `parking_lot::Mutex` (since `1.0`)
## `CircuitBreaker` never panics on a poisoned lock

`0.9.x` used `std::sync::Mutex` with `.unwrap()` on every
`lock()`. A panic anywhere inside the `execute(|| ...)` closure
would poison the mutex, and every subsequent `execute` would
panic at unwrap. That's the opposite of what an error-handling
crate should ship.

`parking_lot::Mutex` has no poisoning. The cost is one extra
runtime dep, which is acceptable for the correctness guarantee.
`1.0` switched to `parking_lot::Mutex`, which has no poisoning.
`1.1` goes back to `std::sync::Mutex` without the panic: the
closure runs outside the lock (a panicking probe is handled by a
drop guard), the lock only covers short updates of the breaker's
own fields, and every acquisition goes through
`PoisonError::into_inner`, so a poisoned lock is used as-is. That
removed `parking_lot` and its six transitive crates from
every dependent's build.

A successful call admitted in the closed state takes the lock
once (admission); only the half-open probe takes it again to close
the circuit. Measured on 1.1 against 1.0.2 (one breaker, no-op
closure, release build): uncontended `execute` 17% faster on Windows
and 20% faster on Linux; eight threads on one breaker about 47% faster on
Windows and 10% faster on Linux. With the same single-lock code,
`parking_lot` was within 1 ns uncontended, about 20 ns per call
faster under the eight-thread load on Linux and about 80 ns slower
on Windows; next to the I/O a circuit breaker guards, neither
difference matters.

## `CodedError::new` is now allocation-only (since `1.0`)

Expand Down
5 changes: 5 additions & 0 deletions docs/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,11 @@ were carrying `paste` in their own `Cargo.toml` purely to
satisfy `define_errors!` can remove it; `error-forge` carries
the dep transitively.

`1.1.0` replaces `pastey` with an internal macro in
`error-forge-derive` (`error_forge::__private::paste!`); the
`__private::pastey` path no longer exists. Nothing outside
`define_errors!` should use either path.

### 10. `atty` dep removed, MSRV declared `1.81`

Shipped in `0.9.8`, summarised here for completeness:
Expand Down
17 changes: 13 additions & 4 deletions error-forge-derive/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "error-forge-derive"
version = "1.0.2"
version = "1.1.0"
edition = "2021"
rust-version = "1.81"
description = "Derive macros for the error-forge crate"
Expand All @@ -15,10 +15,19 @@ authors = [
[lib]
proc-macro = true

[features]
# `derive` provides `#[derive(ModError)]` and is the only part of this
# crate that needs `syn`, `quote` and `proc-macro2`. It is on by default
# so direct users of this crate keep the derive. `error-forge` depends
# on this crate with default features off: the identifier-pasting macro
# behind `define_errors!` is always compiled and has no dependencies.
default = ["derive"]
derive = ["dep:syn", "dep:quote", "dep:proc-macro2"]

[dependencies]
# Floors checked with `cargo update -Z direct-minimal-versions` (see the
# `minimal-versions` CI job): `syn` 1.0.3 adds `Path::get_ident`, and
# `quote` / `proc-macro2` match what `async-trait` needs in the same graph.
syn = "1.0.3"
quote = "1.0.29"
proc-macro2 = "1.0.63"
syn = { version = "1.0.3", optional = true }
quote = { version = "1.0.29", optional = true }
proc-macro2 = { version = "1.0.63", optional = true }
Loading
Loading