In the life of programs, situations can occur where a request cannot be executed. For example, when developing an API that connects to a database, it may happen that the database server is temporarily unavailable. In such cases, without proper error handling, the only option is a panic, which deteriorates the user experience and can create unexpected states in our system, as it results in an immediate termination of the program’s execution.
One of my favorite features in Rust is that it significantly simplifies error handling. Due to the language’s expressive power, it clearly highlights the points where exceptions might occur.
Let’s assume I want to initialize the database connections in my program. I can do this with the PgPoolManager
’s new
function:
1impl PgPoolManager {
2 pub async fn new(
3 main_database_config: &BasicDatabaseConfig,
4 default_tenant_database_config: &BasicDatabaseConfig,
5 ) -> Result<PgPoolManager, sqlx::Error> {
6 let main_pool = PgPoolOptions::new()
7 .max_connections(main_database_config.max_pool_size())
8 .acquire_timeout(Duration::from_secs(3))
9 .connect(&main_database_config.url())
10 .await?;
11 let default_tenant_pool = PgPoolOptions::new()
12 .max_connections(default_tenant_database_config.max_pool_size())
13 .acquire_timeout(Duration::from_secs(3))
14 .connect(&default_tenant_database_config.url())
15 .await?;
16 Ok(Self {
17 main_pool,
18 default_tenant_pool,
19 tenant_pools: Arc::new(RwLock::new(HashMap::new())),
20 })
21 }
22}
Due to Rust’s type system, the function signature indicates that this is an operation that can sometimes fail because the function’s return value is wrapped in the standard library’s Result type. (In the original code, it’s a anyhow::Result, but that will be a topic for a later post :))
With this information, the developer knows that this function can only be called if error handling code is also written for it.
In this example, if the connection to the database fails, the program informs the administrator about the error, and the user sees a nicely formatted message that communicates this fact:
1
2#[tokio::main]
3async fn main(config: Arc<AppConfig>) -> Result<(), String> {
4 let config = init_config()?;
5 let pool_manager = PgPoolManager::new(
6 config.main_database(),
7 config.default_tenant_database()
8 ).await;
9
10 match pool_manager {
11 Ok(pool_manager) => {
12 pool_manager.some_useful_stuff();
13 Ok(())
14 }
15 Err(e) => {
16 error_notify_admin(e);
17 Err(String::from("Nem sikerült csatlakozni az adatbázishoz"))
18 }
19 }
20}
Just a little bit of panic Link to heading
Unlike many other programming languages, Rust by default requires error handling, but it also provides the option to
explicitly tell it that you want to skip it. You can do this with the .unwrap()
function, which clearly indicates in
the source code that this has happened and looks at you with sad puppy eyes until you handle the error.
Using .unwrap()
in production is strongly discouraged because if it encounters an error, the program will immediately
abandon everything and shut down (or at least cancel the currently running thread without informing the user about what
happened).