1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159
//! # 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:
//!
//! ```rust
//! # use std::io;
//! # use edgedb_errors::{UserError, ErrorKind};
//! 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:
//!
//! ```rust
//! # use edgedb_errors::*;
//! # let err = MissingArgumentError::with_message("test error");
//! 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:
//!
//! ```rust
//! # use edgedb_errors::*;
//! # let err1 = ClientConnectionTimeoutError::with_message("test error");
//! # let err2 = TransactionConflictError::with_message("test error");
//!
//! 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>());
//! ```
//!
//! [`anyhow::Error`]: https://docs.rs/anyhow/latest/anyhow/struct.Error.html
//!
//! # 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:
//! * [`ErrorKind::with_source`] (for any [`std::error::Error`])
//! * [`ErrorKind::with_source_box`] already boxed error
//! * [`ErrorKind::with_source_ref`] for smart wrappers such as
//! [`anyhow::Error`]
//! 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`:
//! ```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:
//! ```rust,no_run
//! #[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:
//! ```rust,no_run
//! 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:
//!
//! ```rust,no_run
//! # async fn do_something() -> anyhow::Result<()> { unimplemented!() }
//! #[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.
//!
//! [miette]: https://crates.io/crates/miette
//! [anyhow]: https://crates.io/crates/anyhow
//!
mod error;
mod traits;
pub mod display;
pub mod kinds;
pub mod fields;
#[cfg(feature="miette")]
pub mod miette;
pub use traits::{ErrorKind, ResultExt, Field};
pub use error::{Error, Tag};
pub use kinds::*;