logo MSJO.kr

Rust, Custom Error Handling

2024-12-21
MsJ

프로그램 언어들은 예외 핸들링(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
  1. 프라부 에스왈라, 러스트서버 서비스 앱만들기, 김모세, 제이펍, 2024, p130
  2. Google, Gemini 2.0 Flash Experimental, “rust thiserror and anyhow 비교”

Prεv(Θld)  
Content
Search     RSS Feed     BY-NC-ND