Skip to content

feat: application level errors#1266

Merged
SantiagoPittella merged 16 commits into
nextfrom
santiagopittella-app-level-errors
Oct 13, 2025
Merged

feat: application level errors#1266
SantiagoPittella merged 16 commits into
nextfrom
santiagopittella-app-level-errors

Conversation

@SantiagoPittella

@SantiagoPittella SantiagoPittella commented Oct 2, 2025

Copy link
Copy Markdown
Collaborator

closes #1106

@SantiagoPittella

Copy link
Copy Markdown
Collaborator Author

@Mirko-von-Leipzig @bobbinth in this draft, besides creating all the enums, I added this errors in the SyncNullifiers endpoint as an example, where I created an internal error enum that is then converted into a tonic::Status struct by passing through the SyncNullifiersGrpcError, as we do in the block producer's (SubmitProvenBatch & SubmitProvenTransaction).

Additionally, should I create a trait for the methods implemented by the *GrpcError enums? That way we could implement at least one default implementation easily (tonic_code).

@Mirko-von-Leipzig Mirko-von-Leipzig left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this largely makes sense

Comment thread crates/proto/src/errors/store.rs Outdated
Comment thread crates/proto/src/errors/store.rs Outdated
Comment thread crates/proto/src/errors/store.rs Outdated

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we create a macro_rules to create these enums?

grpc_error!(
    SyncNullifiersGrpcError {
        InvalidBlockRange,
        InvalidPrefixLength,
    }
);

which generates

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[repr(u8)]
pub enum SyncNullifiersGrpcError {
    Internal = 0,
    DeserializationFailed = 1,
    InvalidBlockRange = 2,
    InvalidPrefixLength = 3,
}

impl GrpcError for SyncNullifiersGrpcError {
    fn api_code(self) -> u8 {
        self as u8
    }

    fn is_internal(&self) -> bool {
        self == &Self::Internal
    }
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Due to the variable length of variants for each enum, I kept the manual enum definition and added the macro for the trait implementation. This is what I did first:

#[allow(unused_macros)]
macro_rules! grpc_error {
    ($error:ident {
        $($field:ident),* $(,)?
    }) => {
        grpc_error!(@build_enum $error, 2, $($field),*);
    };
    
    // Build the enum with proper discriminant assignment
    (@build_enum $error:ident, $next:expr, $field:ident) => {
        #[derive(Debug, Clone, Copy, PartialEq, Eq)]
        #[repr(u8)]
        pub enum $error {
            // Internal Server Error
            Internal = 0,
            // Malformed data
            DeserializationFailed = 1,
            $field = $next,
        }

        impl GrpcError for $error {
            fn api_code(self) -> u8 {
                self as u8
            }

            fn is_internal(&self) -> bool {
                self == &Self::Internal
            }
        }
    };
    
    (@build_enum $error:ident, $next:expr, $field1:ident, $field2:ident) => {
        #[derive(Debug, Clone, Copy, PartialEq, Eq)]
        #[repr(u8)]
        pub enum $error {
            // Internal Server Error
            Internal = 0,
            // Malformed data
            DeserializationFailed = 1,
            $field1 = $next,
            $field2 = $next + 1,
        }

        impl GrpcError for $error {
            fn api_code(self) -> u8 {
                self as u8
            }

            fn is_internal(&self) -> bool {
                self == &Self::Internal
            }
        }
    };

And so on for more variants

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice if we could ensure we are doing this sort of error handling correctly for the user-facing RPC endpoints.

This would require some further thought, but basically instead of a Result<T, tonic::Status> we would want Result<T, E> where E is our own error type that then gets converted to a Status. Doing this cleanly isn't straight-forward though.

Also ideal would be if this error handling was only done at the RPC component since that's the "user" access point. But I think we have to do it in the store and block-producer since they're the only ones with full error info.

This isn't really a concern for this PR; more just musings on my part (so you can ignore this).

@SantiagoPittella SantiagoPittella changed the title wip: application level errors feat: application level errors Oct 3, 2025
@SantiagoPittella SantiagoPittella marked this pull request as ready for review October 3, 2025 21:01
Comment on lines +240 to +246
if block_range.start() > block_range.end() {
return Err(InvalidBlockRange::StartGreaterThanEnd {
start: *block_range.start(),
end: *block_range.end(),
}
.into());
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be an error? Or can we just ignore this e.g. in Rust its just an empty range/iterator.

(5..0).len() == 0

Comment thread crates/proto/src/errors/mod.rs Outdated
Comment on lines +117 to +139
/// Implement Into<tonic::Status> for a given error type
#[macro_export]
macro_rules! into_tonic_status {
($error:ty) => {
impl From<$error> for tonic::Status {
fn from(value: $error) -> Self {
let api_error = value.api_error();

let message = if api_error.is_internal() {
"Internal error".to_owned()
} else {
value.as_report()
};

tonic::Status::with_details(
api_error.tonic_code(),
message,
vec![api_error.api_code()].into(),
)
}
}
};
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be a method on GrpcError I think?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm not sure, GrpcError is implemented for this new *GrpcErrors. While into_tonic_status is implemented for the higher-level errors, using the value.as_report() to enrich the status

Comment thread crates/proto/src/errors/mod.rs Outdated
// ================================================================================================

/// Implement the `GrpcError` trait for a given error type
#[macro_export]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to export this? Its internal to the crate iiuc. Though I always gets confused as to whether export means pub or just that its usable outside the module.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The trait methods defined here are called from the store and block-producer crates, inside each domain error Into<tonic::Status> implementation, so we need to export it

Comment thread crates/store/README.md

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the error handling doc go into the proto/readme.md since that is the formal schema?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a meta note, I feel like it makes more sense for our internal crates to reference the schema docs rather than having their own?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the second comment, should this be done for the endpoints as a whole? or just for the errors?

Comment thread crates/store/src/errors.rs Outdated
@SantiagoPittella SantiagoPittella force-pushed the santiagopittella-app-level-errors branch from d5a84b4 to 047cfe6 Compare October 8, 2025 19:58
tonic::Code::InvalidArgument
}
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why couldn't we use the macro for this one? Anything we could change to be able to use it?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did it an applied it in almost all the error, I could not use it in the AddTransactionError since we look for the internal variants in VerifyTxError. To use it there, we should flatten the variants of that error

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, so the problem is here

#[derive(Debug, Error)]
pub enum AddTransactionError {
    #[error("transaction verification failed")]
    VerificationFailed(#[from] VerifyTxError),

I guess its not ideal that in order to use the macro we need flat error enums. I wonder how complex it would be to support the nesting in the macro. Probably be pretty unwieldy.

If it unblocks using the macro for such errors, I would opt to not worry about internal variants and just map them all 1-to-1. But I don't know if that unblocks anything.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since with all other error enums we don´t have this problem of nested error where some variants are internal errors, I think that defining the explicit conversions only for this exceptions (only 1, this one ATM) is fine. We can track if this situation changes and more complex variants are added before changing the approach, wdyt?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 to flattening.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

double checking, you mean to flatten the variants from VerifyTxError into AddTransactionError, right? @drahnr

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea I do think that is probably a reasonable requirement - any error returned via gRPC needs to be flattened.

Comment thread crates/store/src/errors.rs Outdated

@drahnr drahnr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me, mid-term we should discuss how we can trim the manual labor to a minum, i.e. vie exposing more errors directly, or with minimal translation (except for anything DB or secretes related).

@SantiagoPittella SantiagoPittella merged commit 006bb05 into next Oct 13, 2025
6 checks passed
@SantiagoPittella SantiagoPittella deleted the santiagopittella-app-level-errors branch October 13, 2025 20:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refactor Proto Messages for App-level Errors

4 participants