panic (Rust)
abort & unwind
Option & unwrap
Result
Multiple error types
Error Handling: Rust vs Java (Complete Comparison Table)
real-world Rust error-handling patterns
Error handling is the process of handling the possibility of failure. For example, failing to read a file and then continuing to use that bad input would clearly be problematic. Noticing and explicitly managing those errors saves the rest of the program from various pitfalls.
There are various ways to deal with errors in Rust, which are described in the following subchapters. They all have more or less subtle differences and different use cases. As a rule of thumb:
panic (Rust)
Theory
panic! means: “something went seriously wrong, stop this thread.”
Use it for bugs / impossible states, not for normal user input errors.
Example: “index out of bounds”, “this should never happen”, “broken invariant”.
18.2
abort & unwind
Theory
When panic happens, Rust can handle it in two ways:
unwind (default): Rust unwinds the stack, runs Drop (cleanup), then stops (or can be caught in limited cases).
abort: Rust immediately terminates the process (faster binary, no unwinding, no cleanup guarantees).
You choose using Cargo.toml (compile-time):
[profile.release]
panic = "abort"
18.3
Option & unwrap
Theory
Option is used when a value may be missing:
Some(value)
None
unwrap():
returns the value if Some
panics if None
Better alternatives: match, unwrap_or, expect, ? (when converting to Result).
18.4
Result
Theory
Result is used when an operation can fail:
Ok(value)
Err(error)
Rust encourages explicit error handling instead of exceptions.
18.5
Multiple error types
Theory
Sometimes a function can fail in different ways. Common approaches:
Create your own enum error type (best for apps)
Use Box (simple, flexible)
Use libraries like anyhow/thiserror (not needed for basic learning)
18.6
Iterating over Results
Theory
If you have Vec>, you can:
stop on first error: collect::, E>>()
keep only successes: filter_map(Result::ok)
handle each item with match
Full Rust code (covers 18.1 → 18.6)
use std::error::Error;
use std::fmt;
// -------- 18.5 Multiple error types: custom error enum --------
#[derive(Debug)]
enum AppError {
EmptyInput,
NotANumber(String),
TooSmall(i32),
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AppError::EmptyInput => write!(f, "input is empty"),
AppError::NotANumber(s) => write!(f, "not a valid integer: '{}'", s),
AppError::TooSmall(n) => write!(f, "number is too small: {}", n),
}
}
}
impl Error for AppError {}
// 18.4 Result usage: parse + validate
fn parse_and_validate(input: &str) -> Result<i32, AppError> {
let trimmed = input.trim();
if trimmed.is_empty() {
return Err(AppError::EmptyInput);
}
let n: i32 = trimmed
.parse::<i32>()
.map_err(|_| AppError::NotANumber(trimmed.to_string()))?;
if n < 10 {
return Err(AppError::TooSmall(n));
}
Ok(n)
}
// 18.3 Option example
fn find_user(id: i32) -> Option<&'static str> {
match id {
1 => Some("Ashwani"),
2 => Some("Ravi"),
_ => None,
}
}
// 18.2 unwind: catch panic (works only for unwinding panics)
fn demo_catch_unwind() {
println!("--- 18.2 catch_unwind demo (unwind only) ---");
let result = std::panic::catch_unwind(|| {
println!("Inside closure: about to panic...");
panic!("boom!");
});
match result {
Ok(_) => println!("No panic happened"),
Err(_) => println!("Panic was caught (program continues)"),
}
}
// 18.6 Iterating over Results
fn demo_iterating_results() {
println!("--- 18.6 Iterating over Results ---");
let inputs = vec!["12", " 9 ", "abc", "30"];
let parsed: Vec<Result<i32, AppError>> = inputs
.iter()
.map(|s| parse_and_validate(s))
.collect();
println!("Per-item handling (match):");
for (i, r) in parsed.iter().enumerate() {
match r {
Ok(v) => println!(" item {} => Ok({})", i, v),
Err(e) => println!(" item {} => Err({})", i, e),
}
}
// stop on first error:
let all_ok: Result<Vec<i32>, AppError> = inputs.iter().map(|s| parse_and_validate(s)).collect();
println!("collect into Result<Vec<_>> (fails on first Err): {:?}", all_ok);
// keep only successes:
let ok_only: Vec<i32> = inputs
.iter()
.filter_map(|s| parse_and_validate(s).ok())
.collect();
println!("only Ok values: {:?}", ok_only);
}
fn main() {
println!("--- 18.3 Option & unwrap ---");
let u1 = find_user(1);
println!("find_user(1) => {:?}", u1);
println!("unwrap Some => {}", u1.unwrap());
let u9 = find_user(9);
println!("find_user(9) => {:?}", u9);
println!("unwrap_or => {}", u9.unwrap_or("Guest"));
// Don't actually crash the demo; show unwrap panic safely via catch_unwind
let unwrap_panic = std::panic::catch_unwind(|| {
let none_user = find_user(999);
none_user.unwrap(); // would panic
});
println!("unwrap on None panics? {}", unwrap_panic.is_err());
println!("\n--- 18.4 Result ---");
for s in ["15", "8", "xyz", "50"] {
match parse_and_validate(s) {
Ok(v) => println!("input '{}' => Ok({})", s, v),
Err(e) => println!("input '{}' => Err({})", s, e),
}
}
println!();
demo_catch_unwind();
println!("\n--- 18.1 panic (shown safely) ---");
let p = std::panic::catch_unwind(|| {
println!("About to panic now...");
panic!("this is a deliberate panic example");
});
println!("panic happened? {}", p.is_err());
println!("\n--- 18.5 Multiple error types (already shown via AppError enum) ---");
let bad = parse_and_validate(" ");
println!("parse_and_validate(' ') => {:?}", bad);
println!();
demo_iterating_results();
println!("\nNOTE: 18.2 abort vs unwind is chosen at compile-time (Cargo.toml).");
}
Output (Rust)
--- 18.3 Option & unwrap ---
find_user(1) => Some("Ashwani")
unwrap Some => Ashwani
find_user(9) => None
unwrap_or => Guest
unwrap on None panics? true
--- 18.4 Result ---
input '15' => Ok(15)
input '8' => Err(number is too small: 8)
input 'xyz' => Err(not a valid integer: 'xyz')
input '50' => Ok(50)
--- 18.2 catch_unwind demo (unwind only) ---
Inside closure: about to panic...
Panic was caught (program continues)
--- 18.1 panic (shown safely) ---
About to panic now...
panic happened? true
--- 18.5 Multiple error types (already shown via AppError enum) ---
parse_and_validate(' ') => Err(EmptyInput)
--- 18.6 Iterating over Results ---
Per-item handling (match):
item 0 => Ok(12)
item 1 => Err(number is too small: 9)
item 2 => Err(not a valid integer: 'abc')
item 3 => Ok(30)
collect into Result<Vec<_>> (fails on first Err): Err(TooSmall(9))
only Ok values: [12, 30]
NOTE: 18.2 abort vs unwind is chosen at compile-time (Cargo.toml).
Java comparison (exceptions + Optional)
Java uses:
exceptions for errors (try/catch)
Optional for maybe-values
no direct “unwind vs abort” switch like Rust; program ends if exception is uncaught
Full Java code + output
import java.util.*;
import java.util.stream.Collectors;
public class Main {
// 18.5 Multiple error types: custom exception hierarchy
static class AppException extends Exception {
AppException(String msg) { super(msg); }
}
static class EmptyInput extends AppException {
EmptyInput() { super("input is empty"); }
}
static class NotANumber extends AppException {
NotANumber(String s) { super("not a valid integer: '" + s + "'"); }
}
static class TooSmall extends AppException {
TooSmall(int n) { super("number is too small: " + n); }
}
// 18.4 Result equivalent: in Java we throw exceptions
static int parseAndValidate(String input) throws AppException {
String trimmed = input.trim();
if (trimmed.isEmpty()) throw new EmptyInput();
int n;
try {
n = Integer.parseInt(trimmed);
} catch (NumberFormatException e) {
throw new NotANumber(trimmed);
}
if (n < 10) throw new TooSmall(n);
return n;
}
// 18.3 Option equivalent: Optional
static Optional<String> findUser(int id) {
if (id == 1) return Optional.of("Ashwani");
if (id == 2) return Optional.of("Ravi");
return Optional.empty();
}
public static void main(String[] args) {
System.out.println("--- 18.3 Optional & unwrap-like ---");
Optional<String> u1 = findUser(1);
System.out.println("findUser(1) => " + u1);
System.out.println("get() => " + u1.get());
Optional<String> u9 = findUser(9);
System.out.println("findUser(9) => " + u9);
System.out.println("orElse => " + u9.orElse("Guest"));
try {
u9.get(); // throws NoSuchElementException
} catch (NoSuchElementException e) {
System.out.println("get() on empty throws? true");
}
System.out.println("\n--- 18.4 Result (exceptions) ---");
for (String s : new String[]{"15", "8", "xyz", "50"}) {
try {
int v = parseAndValidate(s);
System.out.println("input '" + s + "' => Ok(" + v + ")");
} catch (AppException e) {
System.out.println("input '" + s + "' => Err(" + e.getMessage() + ")");
}
}
System.out.println("\n--- 18.1 panic equivalent: RuntimeException ---");
try {
System.out.println("About to throw RuntimeException...");
throw new RuntimeException("this is a deliberate crash example");
} catch (RuntimeException e) {
System.out.println("crash happened? true");
}
System.out.println("\n--- 18.6 Iterating over results ---");
List<String> inputs = Arrays.asList("12", " 9 ", "abc", "30");
System.out.println("Per-item handling:");
List<Integer> okOnly = new ArrayList<>();
for (int i = 0; i < inputs.size(); i++) {
try {
int v = parseAndValidate(inputs.get(i));
System.out.println(" item " + i + " => Ok(" + v + ")");
okOnly.add(v);
} catch (AppException e) {
System.out.println(" item " + i + " => Err(" + e.getMessage() + ")");
}
}
System.out.println("only Ok values: " + okOnly);
System.out.println("\nNOTE: Java has no compile-time abort/unwind option like Rust.");
}
}
Output (Java)
--- 18.3 Optional & unwrap-like ---
findUser(1) => Optional[Ashwani]
get() => Ashwani
findUser(9) => Optional.empty
orElse => Guest
get() on empty throws? true
--- 18.4 Result (exceptions) ---
input '15' => Ok(15)
input '8' => Err(number is too small: 8)
input 'xyz' => Err(not a valid integer: 'xyz')
input '50' => Ok(50)
--- 18.1 panic equivalent: RuntimeException ---
About to throw RuntimeException...
crash happened? true
--- 18.6 Iterating over results ---
Per-item handling:
item 0 => Ok(12)
item 1 => Err(number is too small: 9)
item 2 => Err(not a valid integer: 'abc')
item 3 => Ok(30)
only Ok values: [12, 30]
NOTE: Java has no compile-time abort/unwind option like Rust.
Error Handling: Rust vs Java (Complete Comparison Table)
Real-world Rust error-handling patterns
use std::collections::HashMap;
use std::error::Error;
use std::fmt;
// -----------------------------
// Low-level errors (Repo layer)
// -----------------------------
#[derive(Debug)]
enum RepoError {
MissingKey { key: String },
}
impl fmt::Display for RepoError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RepoError::MissingKey { key } => write!(f, "missing config key: {}", key),
}
}
}
impl Error for RepoError {}
// -----------------------------
// Service layer errors (wrap RepoError)
// -----------------------------
#[derive(Debug)]
enum ServiceError {
ConfigLoad { source: RepoError },
InvalidPort { value: String },
UserNotFound { user_id: i32 },
}
impl fmt::Display for ServiceError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ServiceError::ConfigLoad { source } => write!(f, "failed to load config: {}", source),
ServiceError::InvalidPort { value } => write!(f, "invalid port value: '{}'", value),
ServiceError::UserNotFound { user_id } => write!(f, "user not found: id={}", user_id),
}
}
}
impl Error for ServiceError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
ServiceError::ConfigLoad { source } => Some(source),
_ => None,
}
}
}
// Convert RepoError -> ServiceError automatically so `?` works across layers
impl From<RepoError> for ServiceError {
fn from(e: RepoError) -> Self {
ServiceError::ConfigLoad { source: e }
}
}
// -----------------------------
// "Repository" (pretend config store / DB)
// -----------------------------
fn repo_get(config: &HashMap<String, String>, key: &str) -> Result<String, RepoError> {
config.get(key).cloned().ok_or_else(|| RepoError::MissingKey {
key: key.to_string(),
})
}
// -----------------------------
// Service layer: uses `?` + adds validation
// -----------------------------
fn service_load_port(config: &HashMap<String, String>) -> Result<u16, ServiceError> {
// `?` returns ServiceError (via From<RepoError>)
let port_str = repo_get(config, "PORT")?;
let port_num: u16 = port_str
.parse::<u16>()
.map_err(|_| ServiceError::InvalidPort { value: port_str.clone() })?;
Ok(port_num)
}
fn service_get_username(users: &HashMap<i32, String>, user_id: i32) -> Result<String, ServiceError> {
users
.get(&user_id)
.cloned()
.ok_or(ServiceError::UserNotFound { user_id })
}
// -----------------------------
// "API/CLI layer": friendly printing
// -----------------------------
fn run_app(config: HashMap<String, String>, users: HashMap<i32, String>, user_id: i32) -> Result<(), ServiceError> {
let port = service_load_port(&config)?; // `?` propagate with context
let username = service_get_username(&users, user_id)?; // `?` propagate
println!("Server will start on port: {}", port);
println!("Hello, {}!", username);
Ok(())
}
fn main() {
// Case A: OK
println!("--- Case A: Success ---");
let mut config_ok = HashMap::new();
config_ok.insert("PORT".to_string(), "8080".to_string());
let mut users = HashMap::new();
users.insert(1, "Ashwani".to_string());
match run_app(config_ok, users.clone(), 1) {
Ok(_) => println!("App finished successfully\n"),
Err(e) => print_error_chain(e),
}
// Case B: Missing config (layered error)
println!("--- Case B: Missing PORT (layered error) ---");
let config_missing = HashMap::new();
match run_app(config_missing, users.clone(), 1) {
Ok(_) => println!("App finished successfully\n"),
Err(e) => print_error_chain(e),
}
// Case C: Invalid port (validation error)
println!("\n--- Case C: Invalid PORT ---");
let mut config_bad_port = HashMap::new();
config_bad_port.insert("PORT".to_string(), "eightythousand".to_string());
match run_app(config_bad_port, users.clone(), 1) {
Ok(_) => println!("App finished successfully\n"),
Err(e) => print_error_chain(e),
}
// Case D: User not found
println!("\n--- Case D: User not found ---");
let mut config_ok2 = HashMap::new();
config_ok2.insert("PORT".to_string(), "3000".to_string());
match run_app(config_ok2, users, 999) {
Ok(_) => println!("App finished successfully\n"),
Err(e) => print_error_chain(e),
}
}
// Print layered errors (top error + source chain)
fn print_error_chain(mut e: ServiceError) {
println!("ERROR: {}", e);
let mut current: &dyn Error = &e;
while let Some(src) = current.source() {
println!(" caused by: {}", src);
current = src;
}
}
Output (Rust)
--- Case A: Success ---
Server will start on port: 8080
Hello, Ashwani!
App finished successfully
--- Case B: Missing PORT (layered error) ---
ERROR: failed to load config: missing config key: PORT
caused by: missing config key: PORT
--- Case C: Invalid PORT ---
ERROR: invalid port value: 'eightythousand'
--- Case D: User not found ---
ERROR: user not found: id=999
Error.rs
Function defination
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
Json
};
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Serialize, Deserialize)]
pub struct ErrorResponse {
pub status: String,
pub message: String,
}
impl fmt::Display for ErrorResponse {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", serde_json::to_string(&self).unwrap())
}
}
#[derive(Debug, PartialEq)]
pub enum ErrorMessage {
EmptyPassword,
ExceededMaxPasswordLength(usize),
InvalidHashFormat,
HashingError,
InvalidToken,
ServerError,
WrongCredentials,
EmailExist,
UserNoLongerExist,
TokenNotProvided,
PermissionDenied,
UserNotAuthenticated,
}
impl ToString for ErrorMessage {
fn to_string(&self) -> String {
self.to_str().to_owned()
}
}
impl ErrorMessage {
fn to_str(&self) -> String {
match self {
ErrorMessage::ServerError => "Server Error. Please try again later".to_string(),
ErrorMessage::WrongCredentials => "Email or password is wrong".to_string(),
ErrorMessage::EmailExist => "A user with this email already exists".to_string(),
ErrorMessage::UserNoLongerExist => "User belonging to this token no longer exists".to_string(),
ErrorMessage::EmptyPassword => "Password cannot be empty".to_string(),
ErrorMessage::HashingError => "Error while hashing password".to_string(),
ErrorMessage::InvalidHashFormat => "Invalid password hash format".to_string(),
ErrorMessage::ExceededMaxPasswordLength(max_length) => format!("Password must not be more than {} characters", max_length),
ErrorMessage::InvalidToken => "Authentication token is invalid or expired".to_string(),
ErrorMessage::TokenNotProvided => "You are not logged in, please provide a token".to_string(),
ErrorMessage::PermissionDenied => "You are not allowed to perform this action".to_string(),
ErrorMessage::UserNotAuthenticated => "Authentication required. Please log in.".to_string(),
}
}
}
#[derive(Debug,Clone)]
pub struct HttpError {
pub message: String,
pub status: StatusCode,
}
impl HttpError {
pub fn new(message: impl Into<String>, status: StatusCode) -> Self {
HttpError {
message: message.into(),
status,
}
}
pub fn server_error(message: impl Into<String>) -> Self {
HttpError {
message: message.into(),
status: StatusCode::INTERNAL_SERVER_ERROR,
}
}
pub fn bad_request(message: impl Into<String>) -> Self {
HttpError {
message: message.into(),
status: StatusCode::BAD_REQUEST,
}
}
pub fn unique_constraint_violation(message: impl Into<String>) -> Self {
HttpError {
message: message.into(),
status: StatusCode::CONFLICT
}
}
pub fn unauthorized(message: impl Into<String>) -> Self {
HttpError {
message: message.into(),
status: StatusCode::UNAUTHORIZED,
}
}
pub fn into_http_response(self) -> Response {
let json_response = Json(ErrorResponse {
status: "fail".to_string(),
message: self.message.clone(),
});
(self.status, json_response).into_response()
}
}
impl fmt::Display for HttpError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"HttpError: message: {}, status: {}",
self.message, self.status
)
}
}
impl std::error::Error for HttpError {}
impl IntoResponse for HttpError {
fn into_response(self) -> Response {
self.into_http_response()
}
}
How to call function
use axum::{Json};
use serde::Deserialize;
use crate::error::{HttpError, ErrorMessage};
#[derive(Deserialize)]
struct LoginRequest {
email: String,
password: String,
}
pub async fn login_handler(
Json(payload): Json<LoginRequest>,
) -> Result<Json<String>, HttpError> {
if payload.password.is_empty() {
return Err(
HttpError::bad_request(ErrorMessage::EmptyPassword.to_string())
);
}
if payload.password.len() > 20 {
return Err(
HttpError::bad_request(
ErrorMessage::ExceededMaxPasswordLength(20).to_string()
)
);
}
// success response
Ok(Json("Login successful".to_string()))
}
Top comments (0)