- 에러처리 실패 사례
- 실패 사례 개선
- thiserror 기본 사용법 1
- thiserror 기본 사용법 2
- anyhow 기본 사용법
- thiserror, anyhow 요약
- Reference
프로그램 언어들은 예외 핸들링(exception handling) 또는 반환 값(return value) 이라는 두 가지 에러 핸들링 접근 방식 중 한 가지를 사용한다. Rust는 후자를 사용한다.1 이전 글 Rust-004, 러스트 예외/에러 처리에서 복구 가능한 에러를 위한 Result<T, E>
사용법을 살펴봤다.
Rust의 Result는 한가지 에러 타입만 처리가 기본적으로 가능하다. 두 가지 이상의 다른 에러 타입은 처리가 불가능할 때 사용할 수 있는 Custom Error Handling 방법을 이번 글에서 살펴본다. 에러처리 간소화를 위한 thiserror, anyhow 크레이트 예제를 소개하였다.
에러처리 실패 사례
use std::fs::File;
use std::io::Write;
use std::num::ParseIntError;
fn main() {
println!("{:?}", square("2"));
println!("{:?}", square("invalid"));
}
fn square(val: &str) -> Result<i32, ParseIntError> {
let num = val.parse::<i32>()?;
let mut f = File::open("file.txt")?;
let string_to_write = format!("Square of {} is {}", num, i32::pow(num, 2));
f.write(string_to_write.as_bytes())?;
Ok(i32::pow(num, 2))
}
/*
the trait `From<std::io::Error>` is not implemented for `ParseIntError`
ParseIntError, Error : 에러 타입이 두 가지지만 하나의 에러 타입만 지정
*/
실패 사례 개선
use std::error::Error;
use std::fmt::{Display, Formatter, Result as FmtResult};
use std::fs::File;
use std::io::Write;
#[derive(Debug)]
enum MyError {
ParseError,
IOError,
}
impl Display for MyError {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
match self {
MyError::ParseError => write!(f, "Parse Error"),
MyError::IOError => write!(f, "IO Error"),
}
}
}
impl Error for MyError {}
fn main() {
let result1 = square("2");
let result2 = square("invalid");
match result1 {
Ok(res) => println!("Result1 is {:?}", res),
Err(e) => println!("Error1 is {:?}", e),
}
match result2 {
Ok(res) => println!("Result2 is {:?}", res),
Err(e) => println!("Error2 is {:?}", e),
}
}
fn square(val: &str) -> Result<i32, MyError> {
let num = val.parse::<i32>().map_err(|_| MyError::ParseError)?;
let mut f = File::open("file.txt").map_err(|_| MyError::IOError)?;
let string_to_write = format!("Square of {} is {}", num, i32::pow(num, 2));
f.write(string_to_write.as_bytes()).map_err(|_| MyError::IOError)?;
Ok(i32::pow(num, 2))
}
/*
Error1 is IOError
Error2 is ParseError
*/
use std::fmt;
use std::error::Error;
#[derive(Debug)]
struct MyError {
message: String,
}
impl fmt::Display for MyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "MyError: {}", self.message)
}
}
impl Error for MyError {}
fn fallible_function() -> Result<(), MyError> {
Err(MyError { message: "Something went wrong".to_string() })
}
fn main() {
match fallible_function() {
Ok(_) => println!("Success!"),
Err(e) => println!("Error: {}", e),
}
}
//Error: MyError: Something went wrong
thiserror 기본 사용법 1
use thiserror::Error;
#[derive(Error, Debug)]
pub enum MyError {
#[error("Invalid argument: {0}")]
InvalidArgument(String),
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
#[error("Parse error: {0}")]
ParseError(#[from] std::num::ParseIntError),
#[error("Custom error message")]
CustomError,
}
fn fallible_function() -> Result<(), MyError> {
Err(MyError::InvalidArgument("Invalid input".to_string()))
}
fn main() {
match fallible_function() {
Ok(_) => println!("Success!"),
Err(e) => println!("Error: {}", e),
}
}
// Error: Invalid argument: Invalid input
thiserror 기본 사용법 2
use thiserror::Error;
#[derive(Error, Debug)]
pub enum MyError {
#[error("Failed to open file: {0}")]
OpenFileError(String),
#[error("Failed to read file: {0}")]
ReadFileError(String),
#[error("Invalid number format: {0}")]
ParseIntError(#[from] std::num::ParseIntError),
#[error("Division by zero")]
DivisionByZero,
}
fn read_number_from_file(path: &str) -> Result<i32, MyError> {
let file_content = std::fs::read_to_string(path).map_err(|_| MyError::OpenFileError(path.to_string()))?;
let number = file_content.trim().parse::<i32>().map_err(MyError::from)?;
Ok(number)
}
fn divide(x: i32, y: i32) -> Result<i32, MyError> {
if y == 0 {
return Err(MyError::DivisionByZero);
}
Ok(x / y)
}
fn main() {
// 성공 케이스
match read_number_from_file("number.txt") {
Ok(number) => println!("File read successfully. Number: {}", number),
Err(e) => eprintln!("Error reading file: {}", e),
}
match divide(10, 2) {
Ok(result) => println!("Division successful. Result: {}", result),
Err(e) => eprintln!("Error during division: {}", e),
}
match read_number_from_file("non_existent_file.txt") {
Ok(number) => println!("File read successfully. Number: {}", number),
Err(e) => eprintln!("Error reading file: {}", e),
}
match read_number_from_file("invalid_number.txt") {
Ok(number) => println!("File read successfully. Number: {}", number),
Err(e) => eprintln!("Error reading file: {}", e),
}
match divide(10, 0) {
Ok(result) => println!("Division successful. Result: {}", result),
Err(e) => eprintln!("Error during division: {}", e),
}
}
/*
Error reading file: Failed to open file: number.txt
Division successful. Result: 5
Error reading file: Failed to open file: non_existent_file.txt
Error reading file: Failed to open file: invalid_number.txt
Error during division: Division by zero
*/
anyhow 기본 사용법
use anyhow::{anyhow, Context, Result as AnyResult};
use std::fs::File;
use std::io::Read;
fn divide(x: i32, y: i32) -> AnyResult<i32> {
if y == 0 {
return Err(anyhow!("Division by zero"));
}
Ok(x / y)
}
fn read_file(path: &str) -> AnyResult<String> {
let mut file = File::open(path).with_context(|| format!("Failed to open file: {}", path))?;
let mut contents = String::new();
file.read_to_string(&mut contents).with_context(|| format!("Failed to read file: {}", path))?;
Ok(contents)
}
fn parse_number(s: &str) -> AnyResult<i32> {
s.parse::<i32>().map_err(|e| anyhow!(e))
}
fn main() -> AnyResult<()> {
let result = divide(10, 2)?;
println!("Result: {}", result);
if let Err(e) = divide(10, 0) {
eprintln!("Divide Error: {}", e);
}
match read_file("file.txt") {
Ok(contents) => println!("{}", contents),
Err(e) => eprintln!("Read File Error: {}", e),
}
let num = parse_number("123")?;
println!("Number: {}", num);
if let Err(e) = parse_number("abc") {
eprintln!("Parse Number Error: {}", e);
}
Ok(())
}
/*
Result: 5
Divide Error: Division by zero
Read File Error: Failed to open file: file.txt
Number: 123
Parse Number Error: invalid digit found in string
*/
thiserror, anyhow 요약
라이브러리를 개발하거나 세밀한 처리가 필요한 경우에는 thiserror를 사용하고 테스트를 위해 빠른 개발이 필요하거나 간단한 에러처리에는 anyhow를 사용하는 것을 권장한다.2
특징 | thiserror | anyhow |
---|---|---|
에러 타입 | 명시적인 타입(enum, struct) | anyhow::Error 로 추상화 |
에러 생성 | 다소 복잡 | anyhow! Macros로 간편 |
에러 문맥 | 직접 구현해야 함 | context() 메서드로 간편 |
디스패치 | 정적 | 동적 |
성능 | 약간 더 빠름 | 약간 느릴 수 있음(미미한 수준) |
용도 | 라이브러리 개발, 세밀한 에러 처리, 성능 중시 | 애플리케이션 개발, 빠른 개발, 간단한 에러처리 |
Reference
- 프라부 에스왈라, 러스트서버 서비스 앱만들기, 김모세, 제이펍, 2024, p130
- Google, Gemini 2.0 Flash Experimental, “rust thiserror and anyhow 비교”