From ef9a2995649e88828522573519900da72ecf2cee Mon Sep 17 00:00:00 2001 From: andylokandy Date: Sun, 1 Feb 2026 16:13:02 +0800 Subject: [PATCH 1/2] chore: add an example for library --- examples/Cargo.toml | 8 +- examples/src/antipattern.rs | 30 ++-- examples/src/library-boundary.rs | 266 +++++++++++++++++++++++++++++++ examples/src/make-error.rs | 63 -------- 4 files changed, 289 insertions(+), 78 deletions(-) create mode 100644 examples/src/library-boundary.rs delete mode 100644 examples/src/make-error.rs diff --git a/examples/Cargo.toml b/examples/Cargo.toml index f053462..873bf92 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -34,10 +34,6 @@ path = "src/custom-layout.rs" name = "downcast" path = "src/downcast.rs" -[[example]] -name = "make-error" -path = "src/make-error.rs" - [[example]] name = "into-anyhow" path = "src/into-anyhow.rs" @@ -46,6 +42,10 @@ path = "src/into-anyhow.rs" name = "into-std-error" path = "src/into-std-error.rs" +[[example]] +name = "library-boundary" +path = "src/library-boundary.rs" + [package.metadata.release] release = false diff --git a/examples/src/antipattern.rs b/examples/src/antipattern.rs index f4a8516..15f675c 100644 --- a/examples/src/antipattern.rs +++ b/examples/src/antipattern.rs @@ -42,15 +42,21 @@ struct MainError; impl std::error::Error for MainError {} mod app { + use std::net::IpAddr; + use super::*; pub fn run() -> Result<(), AppError> { - // ❌ ANTI-PATTERN: Describing the HTTP layer's job, not the app layer's purpose - http::send_request().or_raise(|| AppError("failed to send request".to_string()))?; + // CORRECT: Describe what this layer does + // let make_error = || AppError("failed to run app".to_string()); + // let host = "127.0.0.1".parse::().or_raise(make_error)?; + // crate::http::send_request(host).or_raise(make_error)?; - // ✅ CORRECT: Describe what THIS layer does - // crate::http::send_request() - // .or_raise(|| AppError("failed to run app".to_string()))?; + // ANTI-PATTERN: Describing the HTTP layer's job, not the app layer's purpose + let host = "127.0.0.1" + .parse::() + .or_raise(|| AppError("failed to parse host".to_string()))?; + http::send_request(host).or_raise(|| AppError("failed to send request".to_string()))?; Ok(()) } @@ -61,11 +67,13 @@ mod app { } mod http { + use std::net::IpAddr; + use super::*; - pub fn send_request() -> Result<(), HttpError> { + pub fn send_request(host: IpAddr) -> Result<(), HttpError> { bail!(HttpError { - url: "https://anti-pattern.com".to_string(), + url: host.to_string(), }); } @@ -77,11 +85,11 @@ mod http { impl std::error::Error for HttpError {} } -// Output when running `cargo run --example anti_pattern`. -// Notice "failed to send request" appears twice with no new information! +// Output when running `cargo run --example antipattern`. +// Notice "failed to send request" appears twice with no new information. // // Error: fatal error occurred in application, at examples/src/antipattern.rs:35:16 // | -// |-> failed to send request, at examples/src/antipattern.rs:49:30 +// |-> failed to send request, at examples/src/antipattern.rs:59:34 // | -// |-> failed to send request to server: https://anti-pattern.com, at examples/src/antipattern.rs:67:9 +// |-> failed to send request to server: 127.0.0.1, at examples/src/antipattern.rs:75:9 diff --git a/examples/src/library-boundary.rs b/examples/src/library-boundary.rs new file mode 100644 index 0000000..3813c68 --- /dev/null +++ b/examples/src/library-boundary.rs @@ -0,0 +1,266 @@ +// 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. + +//! # Library Boundary Example - Flat Errors at Public API +//! +//! This example shows how a library can: +//! - Use `exn::Result` internally for context-rich errors. +//! - Downcast internal errors at the public API boundary to produce a flat, machine-friendly +//! `LibError`. +//! - Return `Exn` so context is preserved in frames while internal errors stay private. + +use std::error::Error; + +use derive_more::Display; +use exn::Exn; +use exn::Frame; +use exn::Result; +use exn::ResultExt; +use exn::bail; + +fn main() { + demo(429); + eprintln!(); + demo(404); +} + +fn demo(user_id: u64) { + eprintln!("Start demo for user: {user_id}"); + + let mut attempt = 0; + loop { + match library::fetch_profile(user_id) { + Ok(profile) => { + eprintln!("{}: {}", profile.user_id, profile.plan); + return; + } + Err(err) => { + // Retry for errors the library marks as retryable. + if attempt < 3 && err.is_retryable() { + eprintln!("{}", err); + eprintln!("Retryable error, attempting retry #{}", attempt + 1); + eprintln!(); + attempt += 1; + continue; + } + + let action = match err.kind() { + library::LibErrorKind::NotFound => "Return 404", + library::LibErrorKind::RateLimited => "Retried too many times, aborting", + library::LibErrorKind::Internal => "Internal server error", + }; + eprintln!("Action: {action}"); + eprintln!("Error: {err:?}"); + return; + } + } + } +} + +mod library { + use super::*; + + #[derive(Debug)] + pub struct Profile { + pub user_id: u64, + pub plan: String, + } + + #[derive(Debug, Display, Clone, Copy, PartialEq, Eq)] + pub enum LibErrorKind { + NotFound, + RateLimited, + Internal, + } + + #[derive(Debug, Display)] + #[display("{kind}: {message}")] + pub struct LibError { + kind: LibErrorKind, + message: String, + } + + impl LibError { + pub fn kind(&self) -> LibErrorKind { + self.kind + } + + pub fn is_retryable(&self) -> bool { + matches!(self.kind, LibErrorKind::RateLimited) + } + + fn not_found(resource: &'static str, id: u64) -> Self { + Self { + kind: LibErrorKind::NotFound, + message: format!("{resource} {id} not found"), + } + } + + fn rate_limited() -> Self { + Self { + kind: LibErrorKind::RateLimited, + message: "rate limited by upstream".to_string(), + } + } + + fn internal(message: impl Into) -> Self { + Self { + kind: LibErrorKind::Internal, + message: message.into(), + } + } + } + + impl Error for LibError {} + + /// Public API: returns `Exn` while keeping internal errors private. + pub fn fetch_profile(user_id: u64) -> Result { + // Explicit boundary mapping: downcast internal errors into a flat LibError. + service::fetch_profile(user_id).map_err(map_to_lib_error) + } + + fn map_to_lib_error(err: Exn) -> Exn { + let lib_error = if let Some(db_error) = find_error::(&err) { + match db_error { + db::DbError::NotFound { user_id } => LibError::not_found("user", *user_id), + db::DbError::ConnectionDropped => LibError::rate_limited(), + } + } else if let Some(http_error) = find_error::(&err) { + match http_error { + http::HttpError::RateLimited => LibError::rate_limited(), + http::HttpError::Unavailable => LibError::internal("upstream service unavailable"), + } + } else { + LibError::internal("unexpected library error") + }; + + // Context stays in frames; only LibError is public. + err.raise(lib_error) + } + + fn find_error(exn: &Exn) -> Option<&T> { + fn walk(frame: &Frame) -> Option<&T> { + if let Some(err) = frame.error().downcast_ref::() { + return Some(err); + } + frame.children().iter().find_map(walk::) + } + + walk(exn.frame()) + } + + mod service { + use super::*; + + pub fn fetch_profile(user_id: u64) -> Result { + let make_error = || ServiceError(format!("failed to fetch profile for user {user_id}")); + + let user = db::load_user(user_id).or_raise(make_error)?; + let plan = http::fetch_plan(user.plan_id).or_raise(make_error)?; + + Ok(Profile { + user_id: user.user_id, + plan: plan.name, + }) + } + + #[derive(Debug, Display)] + #[display("{_0}")] + pub struct ServiceError(String); + impl Error for ServiceError {} + } + + mod db { + use super::*; + + pub fn load_user(user_id: u64) -> Result { + match user_id { + 404 => bail!(DbError::NotFound { user_id }), + 500 => bail!(DbError::ConnectionDropped), + _ => Ok(UserRow { + user_id, + plan_id: user_id, + }), + } + } + + pub struct UserRow { + pub user_id: u64, + pub plan_id: u64, + } + + #[derive(Debug, Display)] + pub enum DbError { + #[display("no row for user_id {user_id}")] + NotFound { user_id: u64 }, + #[display("database connection dropped")] + ConnectionDropped, + } + impl Error for DbError {} + } + + mod http { + use super::*; + + pub fn fetch_plan(plan_id: u64) -> Result { + match plan_id { + 429 => bail!(HttpError::RateLimited), + 503 => bail!(HttpError::Unavailable), + _ => Ok(Plan { + name: format!("plan-{plan_id}"), + }), + } + } + + pub struct Plan { + pub name: String, + } + + #[derive(Debug, Display)] + pub enum HttpError { + #[display("HTTP 429: too many requests")] + RateLimited, + #[display("HTTP 503: service unavailable")] + Unavailable, + } + impl Error for HttpError {} + } +} + +// Output when running `cargo run -p examples --example library-boundary`: +// +// Start demo for user: 429 +// RateLimited: rate limited by upstream +// Retryable error, attempting retry #1 +// +// RateLimited: rate limited by upstream +// Retryable error, attempting retry #2 +// +// RateLimited: rate limited by upstream +// Retryable error, attempting retry #3 +// +// Action: Retried too many times, aborting +// Error: RateLimited: rate limited by upstream, at examples/src/library-boundary.rs:149:13 +// | +// |-> failed to fetch profile for user 429, at examples/src/library-boundary.rs:170:55 +// | +// |-> HTTP 429: too many requests, at examples/src/library-boundary.rs:218:24 +// +// Start demo for user: 404 +// Action: Return 404 +// Error: NotFound: user 404 not found, at examples/src/library-boundary.rs:149:13 +// | +// |-> failed to fetch profile for user 404, at examples/src/library-boundary.rs:169:47 +// | +// |-> no row for user_id 404, at examples/src/library-boundary.rs:189:24 diff --git a/examples/src/make-error.rs b/examples/src/make-error.rs deleted file mode 100644 index 1d0bfa2..0000000 --- a/examples/src/make-error.rs +++ /dev/null @@ -1,63 +0,0 @@ -// 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. - -//! # `make_error` Pattern - Reduce Boilerplate -//! -//! When a function has several fallible calls, it's common to want one *function-level* context -//! string for all of them. -//! -//! This reduces boilerplate and avoids the antipattern of writing per-caller-site context that -//! simply repeats what the child error already says. - -use std::error::Error; -use std::net::IpAddr; - -use derive_more::Display; -use exn::Result; -use exn::ResultExt; - -fn main() -> Result<(), MainError> { - let _config = load_server_config().or_raise(|| MainError)?; - Ok(()) -} - -fn load_server_config() -> Result<(u16, IpAddr), ConfigError> { - // Use a single, descriptive message for this function. - let make_error = || ConfigError("failed to load server config".to_string()); - - let port = "8080".parse::().or_raise(make_error)?; - let host = "127.0.0.1".parse::().or_raise(make_error)?; - - let _path = "nope".parse::().or_raise(make_error)?; - - Ok((port, host)) -} - -#[derive(Debug, Display)] -#[display("fatal error occurred in application")] -struct MainError; -impl Error for MainError {} - -#[derive(Debug, Display)] -#[display("{_0}")] -struct ConfigError(String); -impl Error for ConfigError {} - -// Output when running `cargo run -p examples --example make-error`: -// -// Error: fatal error occurred in application, at examples/src/make-error.rs:31:40 -// | -// |-> failed to load server config, at examples/src/make-error.rs:42:39 -// | -// |-> invalid digit found in string, at examples/src/make-error.rs:42:39 From 06517dee2dbea1b1d045d3d408229e7cf7a6856f Mon Sep 17 00:00:00 2001 From: andylokandy Date: Sun, 1 Feb 2026 16:25:44 +0800 Subject: [PATCH 2/2] fix --- examples/src/library-boundary.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/src/library-boundary.rs b/examples/src/library-boundary.rs index 3813c68..2e6730b 100644 --- a/examples/src/library-boundary.rs +++ b/examples/src/library-boundary.rs @@ -126,7 +126,7 @@ mod library { /// Public API: returns `Exn` while keeping internal errors private. pub fn fetch_profile(user_id: u64) -> Result { - // Explicit boundary mapping: downcast internal errors into a flat LibError. + // Explicit boundary mapping: downcast internal errors into a flat `LibError`. service::fetch_profile(user_id).map_err(map_to_lib_error) } @@ -145,7 +145,7 @@ mod library { LibError::internal("unexpected library error") }; - // Context stays in frames; only LibError is public. + // Context stays in frames; only `LibError` is public. err.raise(lib_error) }