Skip to content
Merged
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
25 changes: 23 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,29 @@ All notable changes to this project will be documented in this file.
## [Unreleased]

### Added
- `#[error(transparent)]` support in the derive macro: validates wrapper shape,
delegates `Display`/`source` to the inner error, and works with `#[from]`.
- Re-exported `thiserror::Error` as `masterror::Error`, making it possible to
derive domain errors without an extra dependency. The derive supports
`#[from]` conversions, validates `#[error(transparent)]` wrappers, and mirrors
`thiserror`'s ergonomics.
- Added `BrowserConsoleError::context()` for retrieving browser-provided
diagnostics when console logging fails.

### Changed
- README generation now pulls from crate metadata via the build script while
staying inert during `cargo package`, preventing dirty worktrees in release
workflows.

### Documentation
- Documented deriving custom errors via `masterror::Error` and expanded the
browser console section with context-handling guidance.
- Added a release checklist and described the automated README sync process.

### Tests
- Added regression tests covering derive behaviour (including `#[from]` and
transparent wrappers) and ensuring the README stays in sync with its
template.
- Added a guard test that enforces the `AppResult<_>` alias over raw
`Result<_, AppError>` usages within the crate.

## [0.4.0] - 2025-09-15
### Added
Expand Down
60 changes: 57 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ masterror = { version = "0.4.0", default-features = false }
# ] }
~~~

*Unreleased: derive custom errors via `#[derive(Error)]` (`use masterror::Error;`) and inspect browser logging failures with `BrowserConsoleError::context()`.*
*Since v0.4.0: optional `frontend` feature for WASM/browser console logging.*
*Since v0.3.0: stable `AppCode` enum and extended `ErrorResponse` with retry/authentication metadata.*

Expand All @@ -47,8 +48,9 @@ masterror = { version = "0.4.0", default-features = false }
- **Opt-in integrations.** Zero default features; you enable what you need.
- **Clean wire contract.** `ErrorResponse { status, code, message, details?, retry?, www_authenticate? }`.
- **One log at boundary.** Log once with `tracing`.
- **Less boilerplate.** Built-in conversions, compact prelude,
derive macro support for transparent wrappers via `#[error(transparent)]`.
- **Less boilerplate.** Built-in conversions, compact prelude, and the
`masterror::Error` re-export of `thiserror::Error` with `#[from]` /
`#[error(transparent)]` support.
- **Consistent workspace.** Same error surface across crates.

</details>
Expand Down Expand Up @@ -102,6 +104,49 @@ fn do_work(flag: bool) -> AppResult<()> {

</details>

<details>
<summary><b>Derive custom errors</b></summary>

~~~rust
use std::io;

use masterror::Error;

#[derive(Debug, Error)]
#[error("I/O failed: {source}")]
pub struct DomainError {
#[from]
#[source]
source: io::Error,
}

#[derive(Debug, Error)]
#[error(transparent)]
pub struct WrappedDomainError(
#[from]
#[source]
DomainError
);

fn load() -> Result<(), DomainError> {
Err(io::Error::other("disk offline").into())
}

let err = load().unwrap_err();
assert_eq!(err.to_string(), "I/O failed: disk offline");

let wrapped = WrappedDomainError::from(err);
assert_eq!(wrapped.to_string(), "I/O failed: disk offline");
~~~

- `use masterror::Error;` re-exports `thiserror::Error`.
- `#[from]` automatically implements `From<...>` while ensuring wrapper shapes are
valid.
- `#[error(transparent)]` enforces single-field wrappers that forward
`Display`/`source` to the inner error.

</details>

<details>
<summary><b>Error response payload</b></summary>

Expand Down Expand Up @@ -131,14 +176,23 @@ assert_eq!(resp.status, 401);
assert!(payload.is_object());

#[cfg(target_arch = "wasm32")]
err.log_to_browser_console()?;
{
if let Err(console_err) = err.log_to_browser_console() {
eprintln!(
"failed to log to browser console: {:?}",
console_err.context()
);
}
}

Ok(())
}
~~~

- On non-WASM targets `log_to_browser_console` returns
`BrowserConsoleError::UnsupportedTarget`.
- `BrowserConsoleError::context()` exposes optional browser diagnostics for
logging/telemetry when console logging fails.

</details>

Expand Down
60 changes: 57 additions & 3 deletions README.template.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ masterror = { version = "{{CRATE_VERSION}}", default-features = false }
# ] }
~~~

*Unreleased: derive custom errors via `#[derive(Error)]` (`use masterror::Error;`) and inspect browser logging failures with `BrowserConsoleError::context()`.*
*Since v0.4.0: optional `frontend` feature for WASM/browser console logging.*
*Since v0.3.0: stable `AppCode` enum and extended `ErrorResponse` with retry/authentication metadata.*

Expand All @@ -44,8 +45,9 @@ masterror = { version = "{{CRATE_VERSION}}", default-features = false }
- **Opt-in integrations.** Zero default features; you enable what you need.
- **Clean wire contract.** `ErrorResponse { status, code, message, details?, retry?, www_authenticate? }`.
- **One log at boundary.** Log once with `tracing`.
- **Less boilerplate.** Built-in conversions, compact prelude,
derive macro support for transparent wrappers via `#[error(transparent)]`.
- **Less boilerplate.** Built-in conversions, compact prelude, and the
`masterror::Error` re-export of `thiserror::Error` with `#[from]` /
`#[error(transparent)]` support.
- **Consistent workspace.** Same error surface across crates.

</details>
Expand Down Expand Up @@ -96,6 +98,49 @@ fn do_work(flag: bool) -> AppResult<()> {

</details>

<details>
<summary><b>Derive custom errors</b></summary>

~~~rust
use std::io;

use masterror::Error;

#[derive(Debug, Error)]
#[error("I/O failed: {source}")]
pub struct DomainError {
#[from]
#[source]
source: io::Error,
}

#[derive(Debug, Error)]
#[error(transparent)]
pub struct WrappedDomainError(
#[from]
#[source]
DomainError
);

fn load() -> Result<(), DomainError> {
Err(io::Error::other("disk offline").into())
}

let err = load().unwrap_err();
assert_eq!(err.to_string(), "I/O failed: disk offline");

let wrapped = WrappedDomainError::from(err);
assert_eq!(wrapped.to_string(), "I/O failed: disk offline");
~~~

- `use masterror::Error;` re-exports `thiserror::Error`.
- `#[from]` automatically implements `From<...>` while ensuring wrapper shapes are
valid.
- `#[error(transparent)]` enforces single-field wrappers that forward
`Display`/`source` to the inner error.

</details>

<details>
<summary><b>Error response payload</b></summary>

Expand Down Expand Up @@ -125,14 +170,23 @@ assert_eq!(resp.status, 401);
assert!(payload.is_object());

#[cfg(target_arch = "wasm32")]
err.log_to_browser_console()?;
{
if let Err(console_err) = err.log_to_browser_console() {
eprintln!(
"failed to log to browser console: {:?}",
console_err.context()
);
}
}

Ok(())
}
~~~

- On non-WASM targets `log_to_browser_console` returns
`BrowserConsoleError::UnsupportedTarget`.
- `BrowserConsoleError::context()` exposes optional browser diagnostics for
logging/telemetry when console logging fails.

</details>

Expand Down
45 changes: 39 additions & 6 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,8 +203,10 @@ pub use app_error::{AppError, AppResult};
pub use code::AppCode;
pub use kind::AppErrorKind;
pub use response::{ErrorResponse, RetryAdvice};
#[cfg_attr(docsrs, doc(cfg(feature = "derive")))]
/// Derive macro replicating the ergonomics of `thiserror::Error`.
/// Derive macro re-export providing the same ergonomics as `thiserror::Error`.
///
/// Supports `#[from]` conversions and `#[error(transparent)]` wrappers out of
/// the box while keeping compile-time validation of wrapper shapes.
///
/// ```
/// use std::error::Error as StdError;
Expand All @@ -218,11 +220,42 @@ pub use response::{ErrorResponse, RetryAdvice};
/// message: &'static str
/// }
///
/// let err = MiniError {
/// #[derive(Debug, Error)]
/// #[error("wrapper -> {0}")]
/// struct MiniWrapper(
/// #[from]
/// #[source]
/// MiniError
/// );
///
/// #[derive(Debug, Error)]
/// #[error(transparent)]
/// struct MiniTransparent(#[from] MiniError);
///
/// let wrapped = MiniWrapper::from(MiniError {
/// code: 500,
/// message: "boom"
/// };
/// assert_eq!(err.to_string(), "500: boom");
/// assert!(StdError::source(&err).is_none());
/// });
/// assert_eq!(wrapped.to_string(), "wrapper -> 500: boom");
/// assert_eq!(
/// StdError::source(&wrapped).map(|err| err.to_string()),
/// Some(String::from("500: boom"))
/// );
///
/// let expected_source = StdError::source(&MiniError {
/// code: 503,
/// message: "oops"
/// })
/// .map(|err| err.to_string());
///
/// let transparent = MiniTransparent::from(MiniError {
/// code: 503,
/// message: "oops"
/// });
/// assert_eq!(transparent.to_string(), "503: oops");
/// assert_eq!(
/// StdError::source(&transparent).map(|err| err.to_string()),
/// expected_source
/// );
/// ```
pub use thiserror::Error;