feat(errors): add stable error propagation

This commit is contained in:
Onur Özkan committed 2026-08-23 00:40:17 +03:00
1 parent 97acb2a2d7
commit ba9c97ed3b
34 files changed
+1007

No files matched your search

@@ -0,0 +1,149 @@
use kaptan_err_propagate::{ErrorId, ErrorReport, KaptanError};
#[derive(Clone, Copy, Debug, Eq, PartialEq, KaptanError)]
enum TestError {
#[kaptan_error(id = 65001)]
Failed,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, KaptanError)]
enum OuterError {
#[kaptan_error(id = 65002)]
Converted(TestError),
}
impl From<TestError> for OuterError {
fn from(error: TestError) -> Self {
Self::Converted(error)
}
}
fn fail() -> Result<(), TestError> {
Err(TestError::Failed)
}
fn middle() -> Result<(), TestError> {
fail()
}
#[kaptan_err_propagate::propagate]
fn succeeds() -> Result<u8, TestError> {
Ok(7)
}
#[kaptan_err_propagate::propagate]
fn annotated_outer() -> Result<(), TestError> {
middle()?;
Ok(())
}
#[kaptan_err_propagate::propagate]
fn annotated_inner() -> Result<(), TestError> {
fail()
}
fn unannotated_outer() -> Result<(), TestError> {
annotated_inner()
}
#[kaptan_err_propagate::propagate]
fn handles_inner_error() -> Result<(), TestError> {
let _handled = fail().is_err();
Ok(())
}
#[kaptan_err_propagate::propagate]
fn returns_explicit_error(fail_at_boundary: bool) -> Result<(), TestError> {
if fail_at_boundary {
return Err(TestError::Failed);
}
Ok(())
}
type AliasedResult<T> = core::result::Result<T, TestError>;
#[kaptan_err_propagate::propagate]
fn returns_alias() -> AliasedResult<()> {
fail()
}
#[kaptan_err_propagate::propagate]
fn reports_final_converted_error() -> Result<(), OuterError> {
fail()?;
Ok(())
}
#[kaptan_err_propagate::propagate]
fn reports_error_id() -> Result<(), ErrorId> {
Err(ErrorId::new(65003))
}
#[kaptan_err_propagate::propagate]
async fn async_boundary() -> Result<(), TestError> {
fail()?;
Ok(())
}
struct Service;
impl Service {
#[kaptan_err_propagate::propagate]
fn method(&self, fail_at_boundary: bool) -> Result<(), TestError> {
if fail_at_boundary {
fail()?;
}
Ok(())
}
}
fn take_record() -> kaptan_err_propagate::ErrorRecord {
kaptan_err_propagate::take().expect("escaping error must be reported")
}
#[test]
fn reports_only_final_errors_leaving_annotated_boundaries() {
while kaptan_err_propagate::take().is_some() {}
assert_eq!(succeeds(), Ok(7));
assert!(kaptan_err_propagate::take().is_none());
assert_eq!(handles_inner_error(), Ok(()));
assert!(kaptan_err_propagate::take().is_none());
assert_eq!(annotated_outer(), Err(TestError::Failed));
assert_eq!(take_record().id(), TestError::Failed.error_id().get());
assert_eq!(unannotated_outer(), Err(TestError::Failed));
assert_eq!(take_record().id(), TestError::Failed.error_id().get());
assert_eq!(returns_explicit_error(true), Err(TestError::Failed));
assert_eq!(take_record().id(), TestError::Failed.error_id().get());
assert_eq!(returns_alias(), Err(TestError::Failed));
assert_eq!(take_record().id(), TestError::Failed.error_id().get());
assert_eq!(
reports_final_converted_error(),
Err(OuterError::Converted(TestError::Failed))
);
assert_eq!(
take_record().id(),
OuterError::Converted(TestError::Failed).error_id().get()
);
assert_eq!(reports_error_id(), Err(ErrorId::new(65003)));
assert_eq!(take_record().id(), 65003);
assert_eq!(Service.method(false), Ok(()));
assert!(kaptan_err_propagate::take().is_none());
let mut future = core::pin::pin!(async_boundary());
let mut context = core::task::Context::from_waker(core::task::Waker::noop());
assert_eq!(
core::future::Future::poll(future.as_mut(), &mut context),
core::task::Poll::Ready(Err(TestError::Failed))
);
let record = take_record();
assert_eq!(record.id(), TestError::Failed.error_id().get());
assert!(record.raised_at().contains("propagate.rs:"));
}