7 releases (4 breaking)

0.4.1 Jun 22, 2023
0.4.0 May 15, 2023
0.3.0 Jul 26, 2022
0.2.1 May 9, 2022
0.0.0 Oct 27, 2020

#939 in Database interfaces

Download history 61/week @ 2023-11-22 43/week @ 2023-11-29 29/week @ 2023-12-06 39/week @ 2023-12-13 92/week @ 2023-12-20 66/week @ 2023-12-27 66/week @ 2024-01-03 65/week @ 2024-01-10 57/week @ 2024-01-17 44/week @ 2024-01-24 60/week @ 2024-01-31 113/week @ 2024-02-07 157/week @ 2024-02-14 134/week @ 2024-02-21 128/week @ 2024-02-28 77/week @ 2024-03-06

516 downloads per month
Used in 7 crates (4 directly)

MIT/Apache

34KB
669 lines

EdgeDB Rust Binding: Errors Crate

This crate contains definitions of errors returned from the database.

License

Licensed under either of

at your option.


lib.rs:

Error Handling for EdgeDB

All errors that EdgeDB Rust bindings produce are encapsulated into the Error structure. The structure is a bit like Box<dyn Error> or anyhow::Error, except it can only contain EdgeDB error types. Or UserError can be used to encapsulate custom errors (commonly used to return error from the transaction).

Each error kind is represented as a separate type that implements ErrorKind trait. But error kinds are used like marker structs you can use Error::is for error kinds and use them to create instances of the error:

let err = UserError::with_source(io::Error::from(io::ErrorKind::NotFound));
assert!(err.is::<UserError>());

Since errors are hirarhical Error::is works with any ancestor:

assert!(err.is::<MissingArgumentError>());
assert!(err.is::<QueryArgumentError>());  // implied by the assertion above
assert!(err.is::<InterfaceError>());  // and this one
assert!(err.is::<ClientError>());  // and this one

Error hierarchy doesn't have multiple inheritance (i.e. every error has only single parent). When we match across different parents we use error tags:


assert!(err1.is::<ClientConnectionTimeoutError>());
assert!(err2.is::<TransactionConflictError>());
// Both of these are retried
assert!(err1.has_tag(SHOULD_RETRY));
assert!(err2.has_tag(SHOULD_RETRY));

// But they aren't a part of common hierarchy
assert!(err1.is::<ClientError>());
assert!(!err1.is::<ExecutionError>());
assert!(err2.is::<ExecutionError>());
assert!(!err2.is::<ClientError>());

Errors in Transactions

Special care for errors must be taken in transactions. Generally:

  1. Errors from queries should not be ignored, and should be propagagated up to the transaction function.
  2. User errors can be encapsulated into UserError via one of the methods:
  3. Original query error must be propagated via error chain. It can be in the .source() chain but must not be swallowed, otherwise retrying transaction may work incorrectly.

Nice Error Reporting

We use miette crate for including snippets in your error reporting code.

To make it work, first you need enable fancy feature in your top-level crate's Cargo.toml:

[dependencies]
miette = { version="5.3.0", features=["fancy"] }
edgedb-tokio = { version="*", features=["miette-errors"] }

Then if you use miette all the way through your application, it just works:

#[tokio::main]
async fn main() -> miette::Result<()> {
    let conn = edgedb_tokio::create_client().await?;
    conn.query::<String, _>("SELECT 1+2)", &()).await?;
    Ok(())
}

However, if you use some boxed error container (e.g. anyhow), you might need to downcast error for printing:

async fn do_something() -> anyhow::Result<()> {
    let conn = edgedb_tokio::create_client().await?;
    conn.query::<String, _>("SELECT 1+2)", &()).await?;
    Ok(())
}

#[tokio::main]
async fn main() {
    match do_something().await {
        Ok(res) => res,
        Err(e) => {
            e.downcast::<edgedb_tokio::Error>()
                .map(|e| eprintln!("{:?}", miette::Report::new(e)))
                .unwrap_or_else(|e| eprintln!("{:#}", e));
            std::process::exit(1);
        }
    }
}

In some cases, where parts of your code use miette::Result or miette::Report before converting to the boxed (anyhow) container, you might want a little bit more complex downcasting:

#[tokio::main]
async fn main() {
    match do_something().await {
        Ok(res) => res,
        Err(e) => {
            e.downcast::<edgedb_tokio::Error>()
                .map(|e| eprintln!("{:?}", miette::Report::new(e)))
                .or_else(|e| e.downcast::<miette::Report>()
                    .map(|e| eprintln!("{:?}", e)))
                .unwrap_or_else(|e| eprintln!("{:#}", e));
            std::process::exit(1);
        }
    }
}

Note that last two examples do hide error contexts from anyhow and do not pretty print if source() of the error is edgedb_errors::Error but not the top-level one. We leave those more complex cases as an excersize to the reader.

Dependencies

~130–375KB