Designing accessors: Option<Result<T, E>> or Result<T, E>?

I'm designing a data structure to represent CloudEvents in the new official cloudevents/sdk-rust library.

An Event could contain or not a payload, called data. This data field could have different internal representations, depending on how it comes from the wire:

pub enum Data {

While accessing to data, the accessor converts it to the desired type using a TryFrom<Data> impl:

impl TryFrom<Data> for serde_json::Value {
    type Error = serde_json::Error;

    fn try_from(value: Data) -> Result<Self, Self::Error> {
        match value {
            Data::String(s) => Ok(serde_json::from_str(&s)?),
            Data::Binary(v) => Ok(serde_json::from_slice(&v)?),
            Data::Json(v) => Ok(v),

Now, the accessor now looks like:

impl Event {
    pub fn try_get_data<T: Sized + TryFrom<Data, Error = E>, E: std::error::Error>(
    ) -> Option<Result<T, E>> {
        match {
            Some(d) => Some(T::try_from(d.clone())),
            None => None,

Do you think it would be better, from a usability standpoint of view, to have this try_get_data return a Result<T, E>? In that case, E would be custom defined like:

enum DataError {
    ConversionError(Box<dyn std::error::Error>),

If there's a case where it simply returns no data, then I would return a Result<Option<T>, E>


My argument against that is: if there is no data, there is no need to conversion. The Result makes sense only when i try to convert i think...

I mean, if I wanted to use the function, I would probably want to bubble the error up, which I can do with the question mark, but if there is no data, I would probably want to handle that specially. Compare these:

// Result<Option<T>, E>
if let Some(item) = event.try_get_data()? {

// Option<Result<T, E>>
if let Some(fallible) = event.try_get_data() {
    let item = fallible?;

It also makes more semantic sense. The output of the function is Option<T> because it might not exist. The result should wrap the output and allow it to return an error.


Note that you can implement this rather easily with transpose

impl Event {
    pub fn try_get_data<T: Sized + TryFrom<Data>>(
    ) -> Result<Option<T>, T::Error> {|d| T::try_from(d.clone())).transpose()

You also do not need the generic paramter E as you can use the T::Error syntax instead.

I'll go for Result<Option, E>, thanks!

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.