Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor/improve error handling #48

Merged

Conversation

GuillaumeDecMeetsMore
Copy link
Collaborator

@GuillaumeDecMeetsMore GuillaumeDecMeetsMore commented Sep 4, 2024

Changed

  • Improve a few things (but a lot of times) regarding error handling
    • Remove quite a few usages of unwrap() or expect()
      • Make them "deny" by default (lint throws an error, but the lint error can be silenced like eslint by adding a comment)
    • Make all the methods on the repositories to return anyhow::Result<T>
      • As all of these methods call to the DB, all these methods can potentially fail, so Result reflects that

Details

Enums

In Rust, there are a few things to keep in mind when handling "errors" and "lack of values (aka null in other languages)"

Rust has 2 enums

  • Option<T>, which has 2 values => Some(T) or None
    • This is used for handling cases where a value is optional (undefined/null)
  • Result<T, E>, which also has 2 values => Ok(T) or Err(E)
    • This is used for handling cases where something can fail (e.g. a DB call)

So, for example, we can have the following

let normalField: String = "normalField".to_string();
let optionalFieldWithValue: Option<String> = Some("optionalFieldWithValue".to_string());
let optionalFieldWithoutValue: Option<String> = None;

let processSucceeded: Result<String, String> = Ok("Test".to_string());
// Note that the error can be any type
let processFailed: Result<String, String> = Err("Test".to_string());

Unwrap and expect

On there 2 enums, there are quite a few functions that can be used. There are 2 main ones that we need to be careful about

  • unwrap()
  • expect()

Regarding unwrap() and expect(), it does the following

let optionalFieldWithValue: Option<String> = Some("optionalFieldWithValue".to_string());
let optionalFieldWithoutValue: Option<String> = None;

let normalField: String = optionalFieldWithValue.unwrap(); // Same with expect

// However, this will panic, meaning (by default) the app crashes ! (called "panic")
let optionalFieldWithoutValue: Option<String> = None;
let normalField: String = optionalFieldWithoutValue.unwrap();

// `expect` is the same as `unwrap`, just that it allows to provide a custom message
// So this still crashes (panic)
let optionalFieldWithoutValue: Option<String> = None;
let normalField: String = optionalFieldWithoutValue.expect("We didn't have a value !");

So, in short, unwrap() and expect() should be avoided as they make the app to crash.

Regarding these 2 enums, it's worth noting that using ? allows to automatically stop the execution and return the Err(E) or None (but not a panic !).

Anyhow

anyhow is a lib that allows to easily forward errors. In the Rust ecosystems, there are 2 main libs used for handling errors

More details in https://google.github.io/comprehensive-rust/error-handling.html (the whole website is a good resource, it's a tutorial from Google's Android team for learning Rust for their own employees

I'll also add some comments in the code in the PR

@GuillaumeDecMeetsMore GuillaumeDecMeetsMore self-assigned this Sep 4, 2024
@@ -5,6 +5,7 @@ export RUST_BACKTRACE := "1"
install_tools:
cargo install sqlx-cli
cargo install cargo-pretty-test
cargo install cargo-watch
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like the name suggests, it allows to autoreload the app in dev when the source code changes

Comment on lines +1 to +2
allow-unwrap-in-tests = true
allow-expect-in-tests = true
Copy link
Collaborator Author

@GuillaumeDecMeetsMore GuillaumeDecMeetsMore Sep 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We still allow unwrap and expect in tests, as we don't need to be "as safe" in them (just like we sometimes use any in TS tests)

.users
.find(&self.user_id)
.await
.map_err(|_| UseCaseError::InternalError)?;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note this ? at the end

It allows to return directly the error. This is equivalent to the following

let user = match ctx
            .repos
            .users
            .find(&self.user_id)
            .await
            .map_err(|_| UseCaseError::InternalError) {
    Ok(user) => user,
    Err(e) => return Err(e), // Early return in the function     
 };

@GuillaumeDecMeetsMore GuillaumeDecMeetsMore marked this pull request as ready for review September 10, 2024 06:55
@GuillaumeDecMeetsMore GuillaumeDecMeetsMore merged commit dfd31e7 into master Sep 10, 2024
2 checks passed
@GuillaumeDecMeetsMore GuillaumeDecMeetsMore deleted the guillaume/refactor/improve-error-handling branch September 10, 2024 08:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

2 participants