2026년 Rust 에러 핸들링 완벽 가이드: Result, Option, thiserror, anyhow 실전 활용법
Rust의 에러 핸들링을 심층 분석합니다. Result와 Option 타입의 기초부터 thiserror와 anyhow 크레이트 활용, ? 연산자까지 실용적인 코드 예제와 함께 배우는 2026년 최신 가이드입니다.

Rust는 메모리 안전성과 동시성 보장으로 유명한 시스템 프로그래밍 언어입니다. Rust의 에러 핸들링 시스템은 언어 설계의 핵심을 이루며, 2026년 현재 Rust 생태계는 성숙해지고 에러 처리 모범 사례도 확립되었습니다. 이 글에서는 Rust 에러 핸들링의 기초부터 고급 기법까지 실용적인 접근 방식으로 설명합니다.
Rust의 에러 핸들링은 컴파일 타임에 에러 가능성을 강제로 처리하게 함으로써 런타임에서 예기치 않은 크래시를 방지합니다. Result 타입과 Option 타입을 이해하는 것이 견고한 Rust 코드를 작성하는 첫걸음입니다.
Result 타입과 Option 타입의 기초
Rust의 에러 핸들링은 예외 메커니즘이 아닌 타입 시스템을 활용한 접근 방식을 채택하고 있습니다. Result<T, E> 타입과 Option<T> 타입은 Rust 표준 라이브러리에 정의된 열거형으로, 에러나 값의 부재를 명시적으로 표현합니다.
Result 타입의 정의와 사용 방법
use std::fs::File;
use std::io::{self, Read};
fn read_file_contents(path: &str) -> Result<String, io::Error> {
let mut file = File::open(path)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}
fn main() {
match read_file_contents("config.txt") {
Ok(contents) => println!("File contents: {}", contents),
Err(e) => eprintln!("Error reading file: {}", e),
}
}Result<T, E>는 성공 시 Ok(T)를, 실패 시 Err(E)를 반환합니다. 이러한 명시적인 타입으로 인해 호출하는 쪽에서 에러 가능성을 무시할 수 없습니다.
Option 타입을 통한 값 부재 표현
fn find_user_by_id(users: &[User], id: u64) -> Option<&User> {
users.iter().find(|user| user.id == id)
}
struct User {
id: u64,
name: String,
}
fn main() {
let users = vec![
User { id: 1, name: String::from("Alice") },
User { id: 2, name: String::from("Bob") },
];
match find_user_by_id(&users, 1) {
Some(user) => println!("Found user: {}", user.name),
None => println!("User not found"),
}
}Option<T>은 값이 존재하는 경우 Some(T)를, 존재하지 않는 경우 None을 반환합니다. 널 포인터 문제를 타입 시스템 레벨에서 해결합니다.
? 연산자를 통한 에러 전파
? 연산자는 Rust 에러 핸들링에서 가장 중요한 기능 중 하나입니다. 이 연산자를 통해 에러 처리 보일러플레이트 코드를 대폭 줄일 수 있습니다.
use std::fs::File;
use std::io::{self, Read, Write};
fn copy_file_contents(src: &str, dst: &str) -> Result<(), io::Error> {
let mut source = File::open(src)?;
let mut destination = File::create(dst)?;
let mut buffer = Vec::new();
source.read_to_end(&mut buffer)?;
destination.write_all(&buffer)?;
Ok(())
}? 연산자는 Result가 Ok인 경우 내부 값을 추출하고, Err인 경우 즉시 함수에서 반환합니다. 기존의 match 표현식이나 try! 매크로와 비교하여 코드 가독성이 크게 향상됩니다.
? 연산자와 From 트레이트
use std::fs::File;
use std::io::{self, Read};
use std::num::ParseIntError;
#[derive(Debug)]
enum ConfigError {
Io(io::Error),
Parse(ParseIntError),
}
impl From<io::Error> for ConfigError {
fn from(err: io::Error) -> ConfigError {
ConfigError::Io(err)
}
}
impl From<ParseIntError> for ConfigError {
fn from(err: ParseIntError) -> ConfigError {
ConfigError::Parse(err)
}
}
fn read_config_value(path: &str) -> Result<i32, ConfigError> {
let mut file = File::open(path)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
let value: i32 = contents.trim().parse()?;
Ok(value)
}? 연산자는 From 트레이트를 활용하여 에러 타입을 자동으로 변환합니다. 이를 통해 서로 다른 에러 타입을 통일된 방식으로 처리할 수 있습니다.
thiserror를 사용한 커스텀 에러 타입 정의
thiserror 크레이트는 커스텀 에러 타입 정의를 간결하게 만들어주는 derive 매크로를 제공합니다. 2026년 현재 라이브러리 개발에서는 thiserror가 사실상 표준이 되었습니다.
use thiserror::Error;
use std::io;
#[derive(Error, Debug)]
pub enum DatabaseError {
#[error("Connection failed: {0}")]
ConnectionFailed(String),
#[error("Query execution failed: {query}")]
QueryFailed {
query: String,
#[source]
source: io::Error,
},
#[error("Record not found: id={id}")]
NotFound { id: u64 },
#[error(transparent)]
Io(#[from] io::Error),
}
fn execute_query(query: &str) -> Result<Vec<String>, DatabaseError> {
if query.is_empty() {
return Err(DatabaseError::QueryFailed {
query: query.to_string(),
source: io::Error::new(io::ErrorKind::InvalidInput, "Empty query"),
});
}
Ok(vec![String::from("result1"), String::from("result2")])
}thiserror의 주요 특징은 다음과 같습니다:
#[error("...")]속성을 통한 Display 트레이트 자동 구현#[from]속성을 통한From트레이트 자동 구현#[source]속성을 통한 에러 체인 지원#[transparent]속성을 통한 내부 에러의 투명한 표시
anyhow를 사용한 애플리케이션 레벨 에러 핸들링
anyhow 크레이트는 애플리케이션 개발에서 유연한 에러 핸들링을 제공합니다. 라이브러리가 아닌 애플리케이션(바이너리) 코드에서 사용하기에 적합합니다.
use anyhow::{Context, Result, bail, ensure};
use std::fs::File;
use std::io::Read;
fn load_configuration(path: &str) -> Result<Config> {
ensure!(!path.is_empty(), "Configuration path cannot be empty");
let mut file = File::open(path)
.with_context(|| format!("Failed to open configuration file: {}", path))?;
let mut contents = String::new();
file.read_to_string(&mut contents)
.context("Failed to read configuration file")?;
let config: Config = serde_json::from_str(&contents)
.context("Failed to parse configuration JSON")?;
if config.timeout == 0 {
bail!("Invalid configuration: timeout must be greater than 0");
}
Ok(config)
}
#[derive(serde::Deserialize)]
struct Config {
timeout: u64,
host: String,
}anyhow의 주요 기능:
Result<T>타입 별칭 (Result<T, anyhow::Error>)context()와with_context()를 통한 에러 메시지 추가bail!매크로를 통한 즉시 에러 반환ensure!매크로를 통한 조건 검사- 모든 에러 타입을 래핑 가능
thiserror와 anyhow의 사용 구분
두 크레이트는 상호 보완적인 관계에 있으며, 적절한 상황에서 구분하여 사용하는 것이 중요합니다.
// 라이브러리 코드 (thiserror 사용)
mod library {
use thiserror::Error;
#[derive(Error, Debug)]
pub enum LibraryError {
#[error("Invalid input: {0}")]
InvalidInput(String),
#[error("Processing failed")]
ProcessingFailed(#[source] std::io::Error),
}
pub fn process_data(data: &[u8]) -> Result<Vec<u8>, LibraryError> {
if data.is_empty() {
return Err(LibraryError::InvalidInput("Data cannot be empty".into()));
}
Ok(data.to_vec())
}
}
// 애플리케이션 코드 (anyhow 사용)
mod application {
use anyhow::{Context, Result};
use super::library;
pub fn run_application() -> Result<()> {
let data = std::fs::read("input.bin")
.context("Failed to read input file")?;
let processed = library::process_data(&data)
.context("Data processing failed")?;
std::fs::write("output.bin", &processed)
.context("Failed to write output file")?;
Ok(())
}
}사용 구분 기준:
| 용도 | 권장 크레이트 | 이유 | |------|--------------|------| | 라이브러리 개발 | thiserror | 구체적인 에러 타입을 제공하여 사용자가 적절히 핸들링 가능 | | 애플리케이션 개발 | anyhow | 유연한 에러 전파와 컨텍스트 추가가 용이 | | CLI 도구 | anyhow | 에러 메시지 표시가 간결 | | Web API | thiserror + anyhow | 도메인 에러는 thiserror, 핸들러에서는 anyhow |
에러 핸들링 모범 사례
2026년 Rust 생태계에서 다음과 같은 사례가 권장됩니다.
패닉 피하기
// 피해야 할 패턴
fn get_first_element_bad(vec: &[i32]) -> i32 {
vec[0] // 패닉 가능성
}
// 권장 패턴
fn get_first_element_good(vec: &[i32]) -> Option<i32> {
vec.first().copied()
}
// 또는 Result 반환
fn get_first_element_result(vec: &[i32]) -> Result<i32, &'static str> {
vec.first().copied().ok_or("Vector is empty")
}에러 체인 활용
use anyhow::{Context, Result};
use std::fs::File;
use std::io::Read;
fn load_user_preferences(user_id: u64) -> Result<Preferences> {
let path = format!("/home/{}/.config/app/preferences.json", user_id);
let mut file = File::open(&path)
.with_context(|| format!("Failed to open preferences for user {}", user_id))?;
let mut contents = String::new();
file.read_to_string(&mut contents)
.with_context(|| format!("Failed to read preferences file: {}", path))?;
serde_json::from_str(&contents)
.with_context(|| "Failed to parse preferences JSON")
}
#[derive(serde::Deserialize)]
struct Preferences {
theme: String,
}타입 별칭 활용
use thiserror::Error;
pub type Result<T> = std::result::Result<T, AppError>;
#[derive(Error, Debug)]
pub enum AppError {
#[error("Database error: {0}")]
Database(#[from] DatabaseError),
#[error("Network error: {0}")]
Network(#[from] NetworkError),
#[error("Configuration error: {0}")]
Config(String),
}
#[derive(Error, Debug)]
pub enum DatabaseError {
#[error("Connection lost")]
ConnectionLost,
}
#[derive(Error, Debug)]
pub enum NetworkError {
#[error("Timeout")]
Timeout,
}Rust 면접 준비가 되셨나요?
인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.
결론
Rust의 에러 핸들링 시스템은 타입 안전성과 인체공학적 설계를 모두 갖추고 있습니다. Result 타입과 Option 타입을 기반으로, ? 연산자를 통한 간결한 에러 전파, thiserror를 통한 타입 안전한 커스텀 에러 정의, anyhow를 통한 유연한 애플리케이션 레벨 에러 핸들링을 조합하여 견고하고 유지보수하기 쉬운 코드를 구현할 수 있습니다.
라이브러리 개발에서는 thiserror를 사용하여 구체적인 에러 타입을 제공하고, 애플리케이션 개발에서는 anyhow를 활용하여 에러 컨텍스트를 풍부하게 하는 것이 2026년 현재의 모범 사례입니다. 이러한 도구들을 적절히 구분하여 사용함으로써 Rust의 강력한 타입 시스템을 최대한 활용한 에러 핸들링을 구현할 수 있습니다.
공유
관련 기사

Rust 스마트 포인터 완벽 정리: Box, Rc, Arc와 RefCell (2026)
Rust 스마트 포인터 Box, Rc, Arc, RefCell을 컴파일 가능한 2026 예제, 결정 표, 자주 나오는 면접 질문과 함께 설명합니다.

Rust 2026 트레이트와 제네릭 완벽 가이드: 업캐스팅, AsyncFn, 고급 면접 대비
Rust 2024 Edition의 트레이트 오브젝트 업캐스팅, AsyncFn 트레이트, RPITIT 등 신기능을 컴파일 가능한 코드와 함께 설명합니다. 2026년 기술 면접에서 자주 출제되는 고급 질문도 다룹니다.

Rust 웹 프레임워크 비교: Actix Web vs Axum — 2026년 가이드와 면접 대비
Actix Web 4.13과 Axum 0.8의 아키텍처, 성능, 미들웨어 설계를 비교합니다. 2026년 Rust 백엔드 면접에서 자주 나오는 질문과 모범 답변도 함께 다룹니다.