Summary

Enhanced error handling for OpenDAL.

Motivation

OpenDAL didn't handle errors correctly.

fn parse_unexpect_error<E>(_: SdkError<E>, path: &str) -> Error {
    Error::Unexpected(path.to_string())
}

Most time, we return a path that is meaningless for debugging.

There are two issues about this shortcoming:

First, we can't check ErrorKind quickly. We have to use matches for the help:

assert!(
    matches!(
        result.err().unwrap(),
        opendal::error::Error::ObjectNotExist(_)
    ),
);

Then, we didn't bring enough information for users to debug what happened inside OpenDAL.

So we must handle errors correctly, so that:

  • We can check the Kind to know what error happened.
  • We can read context to know more details.
  • We can get the source of this error to know more details.

Guide-level explanation

Now we are trying to get an object's metadata:

let meta = o.metadata().await;

Unfortunately, the Object does not exist, so we can check out what happened.

if let Err(e) = meta {
    if e.kind() == Kind::ObjectNotExist {
        // Handle this error
    }
}

It's possible that we don't care about other errors. It's OK to log it out:

if let Err(e) = meta {
    if e.kind() == Kind::ObjectNotExist {
        // Handle this error
    } else {
        error!("{e}");
    }
}

For a backend implementor, we can provide as much information as possible. For example, we can return bucket is empty to let the user know:

return Err(Error::Backend {
    kind: Kind::BackendConfigurationInvalid,
    context: HashMap::from([("bucket".to_string(), "".to_string())]),
    source: anyhow!("bucket is empty"),
});

Or, we can return an underlying error to let users figure out:

Error::Object {
    kind: Kind::Unexpected,
    op,
    path: path.to_string(),
    source: anyhow::Error::from(err),
}

So our application users will get enough information now:

Object { kind: ObjectNotExist, op: "stat", path: "/tmp/998e4dec-c84b-4164-a7a1-1f140654934f", source: No such file or directory (os error 2) }

Reference-level explanation

We will split Error into Error and Kind.

Kind is an enum organized by different categories.

Every error will map to a kind, which will be in the error message.

pub enum Kind {
    #[error("backend not supported")]
    BackendNotSupported,
    #[error("backend configuration invalid")]
    BackendConfigurationInvalid,

    #[error("object not exist")]
    ObjectNotExist,
    #[error("object permission denied")]
    ObjectPermissionDenied,

    #[error("unexpected")]
    Unexpected,
}

In Error, we will have different struct to carry different contexts:

pub enum Error {
    #[error("{kind}: (context: {context:?}, source: {source})")]
    Backend {
        kind: Kind,
        context: HashMap<String, String>,
        source: anyhow::Error,
    },

    #[error("{kind}: (op: {op}, path: {path}, source: {source})")]
    Object {
        kind: Kind,
        op: &'static str,
        path: String,
        source: anyhow::Error,
    },

    #[error("unexpected: (source: {0})")]
    Unexpected(#[from] anyhow::Error),
}

Every one of them will carry a source: anyhow::Error so that users can get the complete picture of this error. We have implemented Error::kind(), other helper functions are possible, but they are out of this RFC's scope.

pub fn kind(&self) -> Kind {
    match self {
        Error::Backend { kind, .. } => *kind,
        Error::Object { kind, .. } => *kind,
        Error::Unexpected(_) => Kind::Unexpected,
    }
}

The implementor should do their best to carry as much context as possible. Such as, they should return Error::Object to carry the op and path, instead of just returns Error::Unexpected(anyhow::Error::from(err)).

Error::Object {
    kind: Kind::Unexpected,
    op,
    path: path.to_string(),
    source: anyhow::Error::from(err),
}

Drawbacks

None

Rationale and alternatives

Why don't we implement backtrace?

backtrace is not stable yet, and OpenDAL must be compilable on stable Rust.

This proposal doesn't erase the possibility to add support once backtrace is stable.

Prior art

None

Unresolved questions

None

Future possibilities

  • Backtrace support.