“This is the second day of my participation in the First Challenge 2022, for more details: First Challenge 2022”.


In order to send an email correctly, you have to string together several operations into one big function: validate user input, send email, various database queries. They all have one thing in common: they are likely to fail.

Earlier we discussed the cornerstones of error handling in Rust: Result and? .

We left many questions unanswered:

  1. How do errors fit into the broader architecture of the application?
  2. What does a good misexpression look like?
  3. Who are the mistakes for?
  4. Should we use libraries? Which one is used?

Let’s take a closer look at the error handling patterns in Rust.

The nature of Error

Let’s start with an example:

/ /! src/routes/subscriptions.rs
/ / [...].

pub async fn store_token(
    transaction: &mut Transaction<'_, Postgres>,
    subscriber_id: Uuid,
    subscription_token: &str) - >Result<(), sqlx::Error> { sqlx::query! (r#" INSERT INTO subscription_tokens (subscription_token, subscriber_id) VALUES ($1, $2) "#,
        subscription_token,
        subscriber_id
    )
    .execute(transaction)
    .await.map_err(|e| { tracing::error! ("Failed to execute query: {:? }", e); e })? ;Ok(())}Copy the code

We tried to insert a row into table subscription_tokens to store the newly generated tokens corresponding to subscriber_id. Execute is an error-prone operation: there might be a network problem while talking to the database, the row we’re trying to insert might violate some table constraints (such as uniqueness of primary keys), and so on.

Internal Errors

Callers of execute might want to be notified when a failure occurs — they need to react to it accordingly, such as retry the query, or as in our example? .

Rust uses a type system to tell you that an operation may not succeed: The return type of execute is Result, an enumeration.

pub enum Result<Success, Error> {
    Ok(Success),
    Err(Error)
}
Copy the code

The Rust compiler then forces callers to specify how they intend to handle both cases — success or failure.

If our goal is to communicate to the caller that an error has occurred, we can use a simpler Result definition:

pub enum ResultSignal<Success> {
    Ok(Success),
    Err
}
Copy the code

Instead of using the generic Error type, we can just check that execute returns Err, for example:

letoutcome = sqlx::query! (/ *... * /)
    .execute(transaction)
    .await;
if outcome == ResultSignal::Err {
    // Do something if it failed
}
Copy the code

However, if there is only one failure mode, the above works. But the truth is, operations can fail in all sorts of ways, and we might want to react differently depending on what happens. Let’s look at the framework for SQLX ::Error, which is the Error type of execute:

/ /! sqlx-core/src/error.rs

pub enum Error {
    Configuration(/ * * /),
    Database(/ * * /),
    Io(/ * * /),
    Tls(/ * * /),
    Protocol(/ * * /),
    RowNotFound,
    TypeNotFound {/ * * /},
    ColumnIndexOutOfBounds {/ * * /},
    ColumnNotFound(/ * * /),
    ColumnDecode {/ * * /},
    Decode(/ * * /),
    PoolTimedOut,
    PoolClosed,
    WorkerCrashed,
    Migrate(/ * * /),}Copy the code

SQLX ::Error is designed as an enum where the caller can match the returned Error and behave differently depending on the underlying Error classification. For example, you might want to retry PoolTimedOut, or you might ignore ColumnNotFound.