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


Troubleshoot

What if there is only one failure mode for an operation? Should we only use () as the error type?

Err(()) may be enough to let the caller decide what to do. For example, a 500 internal server error is returned to the user.

But control flow is not the only purpose of errors in an application. We want the error to carry enough context about the failure for the developer to produce a * “report” * with enough detail to resolve the problem.

What do we mean by * “report” *?

  • In a back-end API like ours, it’s usually a log.
  • On the CLI, when used--verboseFlag when it may be an error message displayed on the terminal.

The details of the implementation may differ, but the goal is the same: to help developers understand what went wrong. This is exactly what we did in the initial code snippet.

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

pub async fn store_token(/ * * /) - >Result<(), sqlx::Error> { sqlx::query! (/ * * /)
        .execute(transaction)
        .await.map_err(|e| { tracing::error! ("Failed to execute query: {:? }", e); e })? ;/ / [...].
}
Copy the code

If the query fails, we grab the error and generate a log, which we can then check when investigating the database problem if we want to trace the problem back.

The edge error

So far, we have focused on the insides of our API: functions calling other functions, and operators trying to figure out what an error means after it has occurred. But what about users?

Just like the operator, the user expects the API to signal when it encounters an error.

What do users of the API see when store_token() fails? Let’s look at the request handler:

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

pub async fn subscribe(/ * * /) -> HttpResponse {
    / / [...].
    if store_token(&mut transaction, subscriber_id, &subscription_token)
        .await
        .is_err()
    {
        return HttpResponse::InternalServerError().finish();
    }
    / / [...].
}
Copy the code

They receive an HTTP response with no body and a status code with 500 internal server errors.

The status code serves the same purpose as the error type in store_token() : it is a machine-parsed piece of information that the caller (such as a browser) can use to decide what to do next (for example, if this is a short-lived failure, just request a retry).

What about the people behind the browser? What are we gonna tell them?

Nothing. The response body is empty. This is actually a good implementation: users should not care about the internals of the API they are calling. They had no expected model of it, and no way to determine why it failed. That’s the operator. We’ve omitted those details from the design.

In other cases, we need to convey additional information to the user. Let’s look at our input validation for the same terminal:

/ /! src/routes/subscriptions.rs

#[derive(serde::Deserialize)]
pub struct FormData {
    email: String,
    name: String,}impl TryFrom<FormData> for NewSubscriber {
    type Error = String;

    fn try_from(value: FormData) -> Result<Self, Self::Error> {
        let name = SubscriberName::parse(value.name)?;
        letemail = SubscriberEmail::parse(value.email)? ;Ok(Self { email, name })
    }
}
Copy the code

The API receives an E-mail address and a name as data attached to the form submitted by the user. Both fields are subject to additional validation: SubscriberName:: Parse and SubscriberEmail:: Parse. These two methods are error-prone and return a String as an error type to explain the error.

/ /! src/domain/subscriber_email.rs
/ / [...].

impl SubscriberEmail {
    pub fn parse(s: String) - >Result<SubscriberEmail, String> {
        if validate_email(&s) {
            Ok(Self(s))
        } else {
            Err(format!("{} is not a valid subscriber email.", s))
        }
    }
}
Copy the code

I have to admit, this is not the most useful error message: while we tell users that the email address they entered is wrong, we don’t help them determine why. Finally, it doesn’t matter: we didn’t send these error messages to the user as part of the API response. What they get is a 400 bad request with no text.

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

pub async fn subscribe(/ * * /) -> HttpResponse {
    let new_subscriber = match form.0.try_into() {
        Ok(form) => form,
        Err(_) = >return HttpResponse::BadRequest().finish(),
    };
    / / [...].
Copy the code

This is a bad mistake: users are left in the dark and unable to adjust their submission behavior based on the returned information (and don’t understand what’s wrong).

conclusion

Let’s summarize what we’ve found so far. Errors have two main functions:

  1. Control flow (deciding what to do next)
  2. Report (acts as evidence for an after-the-fact investigation, tracing what went wrong)

We can also distinguish between errors based on their location:

  1. Internal error (i.e. one function calls another function in our application)
  2. Edge error (i.e., an API request that we failed to fulfill)

The control flow is programmed, and all the information needed to decide what to do next must be accessible to the machine:

  • We use types (such as enumeration variants), methods, and fields to handle internal errors
  • We rely on the status code to handle edge errors

Instead, error reporting is mostly consumed by humans. Content must be tailored to the user:

  • Developers have access to the internals of the system: they should be provided with as much background as possible on failure patterns.
  • The user is outside the application: the application provides the information necessary to adjust their behavior (for example, to fix bad input).