การจัดการ Error ใน Rust 2026: Result, Option, thiserror และ anyhow

คู่มือการจัดการ error ใน Rust ด้วย Result, Option, ตัวดำเนินการ ?, thiserror และ anyhow สำหรับนักพัฒนาไทย พร้อม best practices ครบถ้วน

การจัดการ Error ใน Rust 2026: Result, Option, thiserror และ anyhow

การจัดการ error ใน Rust ยึดหลักสอง enum—Result<T, E> และ Option<T>—ที่บังคับให้จัดการความสำเร็จ ความล้มเหลว และการไม่มีค่าอย่างชัดเจนในขณะคอมไพล์ ต่างจาก exception ที่แพร่กระจายโดยไม่เห็น แนวทางของ Rust ทำให้เส้นทาง error ปรากฏใน signature ของฟังก์ชัน กำจัดประเภทของข้อผิดพลาด runtime ที่ไม่คาดคิดทั้งหมด ตัวดำเนินการ ? ร่วมกับ crate เช่น thiserror และ anyhow ทำให้โมเดลที่ชัดเจนนี้เรียบง่ายขึ้นโดยไม่สูญเสียความชัดเจน

เมื่อไหร่ควรใช้อะไร

ใช้ Option<T> สำหรับค่าที่อาจไม่มีอย่างถูกต้อง (ฟิลด์การตั้งค่า ผลการค้นหา) ใช้ Result<T, E> เมื่อการดำเนินการอาจล้มเหลวพร้อมข้อมูล error ที่มีความหมาย (I/O ไฟล์ request เครือข่าย การ parsing)

ทำความเข้าใจพื้นฐานของ Result และ Option

Result<T, E> แสดงถึงความสำเร็จ (Ok(T)) หรือความล้มเหลว (Err(E)) Option<T> แสดงถึงการมีค่า (Some(T)) หรือไม่มี (None) ทั้งสองเป็น sum types—compiler รับรองว่าทุก variant ได้รับการจัดการ

error_examples.rsrust
// Demonstrates Result and Option basic patterns

fn find_user(id: u64) -> Option<String> {
    // Returns None if user doesn't exist
    if id == 0 {
        None
    } else {
        Some(format!("User-{}", id))
    }
}

fn parse_port(s: &str) -> Result<u16, std::num::ParseIntError> {
    // Returns Err if parsing fails
    s.parse::<u16>()
}

fn main() {
    // Option handling - must address None case
    match find_user(42) {
        Some(name) => println!("Found: {}", name),
        None => println!("User not found"),
    }

    // Result handling - must address Err case
    match parse_port("8080") {
        Ok(port) => println!("Port: {}", port),
        Err(e) => println!("Invalid port: {}", e),
    }
}

Compiler ปฏิเสธโค้ดที่ละเว้นค่า return เหล่านี้โดยไม่มีการจัดการอย่างชัดเจน การออกแบบนี้จับ bug ในขณะคอมไพล์ที่จะปรากฏเป็น null pointer exception หรือ uncaught error ในภาษาอื่น

ตัวดำเนินการเครื่องหมายคำถามสำหรับการแพร่กระจายที่กระชับ

ตัวดำเนินการ ? แปลงสาย match ที่ยาวเยิ่นยานเป็นโค้ดเชิงเส้นที่อ่านง่าย เมื่อใช้กับ Result มันจะ return ก่อนกำหนดด้วย error ถ้ามี หรือ unwrap ค่าที่สำเร็จ เช่นเดียวกันกับ Option

file_reader.rsrust
// Using ? for clean error propagation

use std::fs::File;
use std::io::{self, BufRead, BufReader};

fn read_first_line(path: &str) -> Result<String, io::Error> {
    let file = File::open(path)?;  // Returns early if open fails
    let mut reader = BufReader::new(file);
    let mut line = String::new();
    reader.read_line(&mut line)?;  // Returns early if read fails
    Ok(line.trim().to_string())
}

fn get_port_from_config(path: &str) -> Result<u16, Box<dyn std::error::Error>> {
    let content = read_first_line(path)?;
    let port = content.parse::<u16>()?;  // ParseIntError converts via From
    Ok(port)
}

ตัวดำเนินการ ? ต้องการประเภท error ที่สามารถแปลงเป็นประเภท error return ของฟังก์ชันผ่าน trait From การใช้ Box<dyn std::error::Error> ตามที่แสดงด้านบนยอมรับประเภท error ใดๆ ที่ implement trait Error มาตรฐาน

สร้าง Custom Error ด้วย thiserror

Crate thiserror กำจัด boilerplate สำหรับประเภท error แบบกำหนดเอง มัน derive การ implement Error, Display, และ From ผ่าน procedural macro

errors.rsrust
// Custom error types using thiserror 2.0

use thiserror::Error;

#[derive(Error, Debug)]
pub enum ConfigError {
    #[error("configuration file not found at {path}")]
    NotFound { path: String },

    #[error("invalid port number: {0}")]
    InvalidPort(#[from] std::num::ParseIntError),

    #[error("IO error reading config")]
    IoError(#[from] std::io::Error),

    #[error("missing required field: {0}")]
    MissingField(String),
}

fn load_config(path: &str) -> Result<Config, ConfigError> {
    let content = std::fs::read_to_string(path)?;  // IoError auto-converts
    let port: u16 = content
        .lines()
        .find(|l| l.starts_with("port="))
        .ok_or(ConfigError::MissingField("port".into()))?
        .strip_prefix("port=")
        .unwrap()
        .parse()?;  // ParseIntError auto-converts to InvalidPort

    Ok(Config { port })
}

struct Config {
    port: u16,
}

Attribute #[from] สร้างการแปลงอัตโนมัติ ช่วยให้ใช้ ? ได้อย่างราบรื่นกับประเภท error พื้นฐานต่างๆ ข้อความ error กลายเป็นเอกสารในตัวผ่าน format string #[error(...)]

Error ระดับแอปพลิเคชันด้วย anyhow

ในขณะที่ thiserror เหมาะกับโค้ด library ที่มีประเภท error เฉพาะ anyhow มุ่งเป้าไปที่แอปพลิเคชันที่บริบทของ error สำคัญกว่าความละเอียดของประเภท Trait Context ของมันเพิ่มข้อความอธิบายให้กับ error ใดๆ

main.rsrust
// Application error handling with anyhow 1.0

use anyhow::{Context, Result, bail, ensure};

fn load_database_url() -> Result<String> {
    std::env::var("DATABASE_URL")
        .context("DATABASE_URL environment variable not set")
}

fn connect_to_database(url: &str) -> Result<DatabaseConnection> {
    ensure!(!url.is_empty(), "database URL cannot be empty");

    let conn = DatabaseConnection::new(url)
        .context("failed to establish database connection")?;

    if !conn.is_healthy() {
        bail!("database connection unhealthy after establishment");
    }

    Ok(conn)
}

fn main() -> Result<()> {
    let url = load_database_url()?;
    let conn = connect_to_database(&url)
        .context("application startup failed")?;

    // Context chains create readable error traces:
    // Error: application startup failed
    // Caused by:
    //     0: failed to establish database connection
    //     1: connection refused

    Ok(())
}

struct DatabaseConnection;
impl DatabaseConnection {
    fn new(_url: &str) -> Result<Self> { Ok(Self) }
    fn is_healthy(&self) -> bool { true }
}

Method context() ห่อ error ด้วยข้อมูลเพิ่มเติม สร้างสายที่ช่วยในการ debug Macro bail! ให้ early exit พร้อมข้อความ error ที่ format แล้ว ขณะที่ ensure! ทำหน้าที่เหมือน assertion ที่ return error แทน panic

พร้อมที่จะพิชิตการสัมภาษณ์ Rust แล้วหรือยังครับ?

ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ

รวม thiserror และ anyhow ในโปรเจกต์จริง

Library เปิดเผย error ที่มีโครงสร้างผ่าน thiserror เพื่อการจัดการแบบ programmatic โดยผู้ใช้ แอปพลิเคชันห่อ error เหล่านั้นด้วย anyhow เพื่อ output ที่มนุษย์อ่านได้ การแยกนี้รักษา API ให้สะอาดในขณะที่คงความสามารถในการ debug

lib.rs - Library code with thiserrorrust
// Exposes typed errors for programmatic handling

use thiserror::Error;

#[derive(Error, Debug)]
pub enum PaymentError {
    #[error("insufficient funds: required {required}, available {available}")]
    InsufficientFunds { required: u64, available: u64 },

    #[error("card declined: {reason}")]
    CardDeclined { reason: String },

    #[error("payment provider unavailable")]
    ProviderUnavailable(#[source] reqwest::Error),
}

pub fn process_payment(amount: u64) -> Result<Receipt, PaymentError> {
    // Library returns specific, matchable error types
    Err(PaymentError::InsufficientFunds {
        required: amount,
        available: 50,
    })
}

pub struct Receipt;
main.rs - Application code with anyhowrust
// Wraps library errors with context

use anyhow::{Context, Result};
use my_payment_lib::{process_payment, PaymentError};

fn checkout(cart_total: u64) -> Result<()> {
    match process_payment(cart_total) {
        Ok(_receipt) => Ok(()),
        Err(PaymentError::InsufficientFunds { required, available }) => {
            // Handle specific case differently
            println!("Add {} to your balance", required - available);
            Ok(())
        }
        Err(e) => Err(e).context("checkout payment processing failed"),
    }
}

Pattern นี้ช่วยให้ caller match กับ variant เฉพาะเมื่อการกู้คืนเป็นไปได้ ในขณะยังคงได้รับประโยชน์จากบริบท error ที่มากเมื่อแพร่กระจายความล้มเหลวขึ้นไปด้านบน ชุมชน Rust ส่วนใหญ่ได้มาตรฐานแนวทางนี้ ตามที่กล่าวถึงใน แนวทาง API Rust

Pattern การจัดการ Error สำหรับโค้ด Async

ฟังก์ชัน async return Result เหมือนกับ synchronous ตัวดำเนินการ ? ทำงานเหมือนกันใน async block และทั้ง thiserror และ anyhow รวมได้โดยไม่ต้องแก้ไข

async_errors.rsrust
// Error handling in async Rust with Tokio

use anyhow::{Context, Result};
use std::time::Duration;

async fn fetch_user_data(user_id: u64) -> Result<UserData> {
    let response = reqwest::get(format!("https://api.example.com/users/{}", user_id))
        .await
        .context("HTTP request to user API failed")?;

    let status = response.status();
    if !status.is_success() {
        anyhow::bail!("user API returned status {}", status);
    }

    let data: UserData = response
        .json()
        .await
        .context("failed to parse user data JSON")?;

    Ok(data)
}

async fn fetch_with_retry(user_id: u64, attempts: u32) -> Result<UserData> {
    let mut last_error = None;

    for attempt in 1..=attempts {
        match fetch_user_data(user_id).await {
            Ok(data) => return Ok(data),
            Err(e) => {
                last_error = Some(e);
                if attempt < attempts {
                    tokio::time::sleep(Duration::from_millis(100 * attempt as u64)).await;
                }
            }
        }
    }

    Err(last_error.unwrap()).context(format!("failed after {} attempts", attempts))
}

#[derive(serde::Deserialize)]
struct UserData {
    name: String,
}

เมื่อรวมการดำเนินการ async หลายรายการ ใช้ try_join! จาก tokio หรือ futures เพื่อรันพร้อมกันในขณะแพร่กระจาย error แรก สำหรับแนวคิดที่เกี่ยวข้อง สำรวจโมดูล คำถามสัมภาษณ์ async/await

Downcasting และการตรวจสอบ Error

ทั้ง anyhow::Error และ Box<dyn Error> รองรับ downcasting เพื่อกู้คืนประเภท error ดั้งเดิม สิ่งนี้ช่วยให้ logging รายละเอียดเฉพาะในขณะยังคงแพร่กระจาย error ทั่วไป

downcasting.rsrust
// Inspecting wrapped error types

use anyhow::{Context, Result};
use std::io;

fn log_and_propagate(result: Result<()>) -> Result<()> {
    if let Err(ref e) = result {
        // Check if the root cause is a specific type
        if let Some(io_err) = e.downcast_ref::<io::Error>() {
            match io_err.kind() {
                io::ErrorKind::NotFound => {
                    tracing::warn!("file not found, using defaults");
                }
                io::ErrorKind::PermissionDenied => {
                    tracing::error!("permission denied - check file ownership");
                }
                _ => {
                    tracing::error!("IO error: {:?}", io_err);
                }
            }
        }
    }
    result
}

Downcasting เชื่อมช่องว่างระหว่างการจัดการ error ทั่วไปและ logic การกู้คืนเฉพาะ ใช้อย่างประหยัด—ถ้า downcasting เกิดขึ้นบ่อย พิจารณาว่า enum error ที่มีประเภทจะดีกว่า

แปลงระหว่าง Option และ Result

Standard library ให้ method สำหรับแปลงระหว่าง Option และ Result ช่วยให้ composition ราบรื่นเมื่อ API ต่างๆ ใช้ pattern ต่างกัน

conversions.rsrust
// Option and Result interoperability

fn get_env_port() -> Option<u16> {
    std::env::var("PORT")
        .ok()  // Result -> Option (discards error)
        .and_then(|s| s.parse().ok())
}

fn get_env_port_with_error() -> Result<u16, String> {
    std::env::var("PORT")
        .map_err(|_| "PORT not set".to_string())?
        .parse()
        .map_err(|_| "PORT is not a valid number".to_string())
}

fn lookup_and_parse(map: &std::collections::HashMap<String, String>, key: &str) -> Result<u16, String> {
    map.get(key)
        .ok_or_else(|| format!("key '{}' not found", key))?  // Option -> Result
        .parse()
        .map_err(|e| format!("parse error for '{}': {}", key, e))
}

Method ok() ทิ้งรายละเอียด error เมื่อเฉพาะการมีอยู่เท่านั้นที่สำคัญ Method ok_or() และ ok_or_else() แปลง None เป็น custom error ช่วยให้ propagation ? จากค่า Option Pattern เหล่านี้ปรากฏบ่อยใน คำถามสัมภาษณ์ pattern matching

ข้อพิจารณาด้านประสิทธิภาพ

การจัดการ error ใน Rust ไม่มีค่าใช้จ่าย runtime บนเส้นทางความสำเร็จ Result และ Option เป็น enum ที่จัดสรรบน stack ที่มีขนาดแน่นอน Compiler optimize การตรวจสอบเมื่อสามารถพิสูจน์ได้ว่า branch ไม่สามารถเข้าถึงได้

| แนวทาง | ค่าใช้จ่ายเส้นทางสำเร็จ | ค่าใช้จ่ายเส้นทางล้มเหลว | |--------|------------------------|------------------------| | Result/Option | ศูนย์ | Stack unwinding (ถูก) | | panic! | ศูนย์ | Full stack unwinding + cleanup | | Exception C++ | ศูนย์ (ปกติ) | การจัดสรร heap แพง + RTTI |

หลีกเลี่ยง unwrap() และ expect() ในโค้ด library—เก็บไว้สำหรับกรณีที่ความล้มเหลวบ่งบอก bug จริงๆ สำหรับเส้นทางที่สำคัญต่อประสิทธิภาพที่ error เกิดขึ้นบ่อย พิจารณาใช้ enum ที่มี data inline แทนประเภท error ที่จัดสรรบน heap

สรุป

  • Result<T, E> จัดการความล้มเหลวที่กู้คืนได้; Option<T> จัดการการไม่มีค่า—ทั้งสองบังคับการจัดการตอนคอมไพล์
  • ตัวดำเนินการ ? แพร่กระจาย error อย่างกระชับ ต้องการ implementation trait From สำหรับการแปลงประเภท
  • thiserror สร้างประเภท error ที่มีโครงสร้างสำหรับ library โดยไม่มี boilerplate
  • anyhow ให้สาย error ตามบริบทสำหรับแอปพลิเคชัน รองรับ context(), bail!, และ ensure!
  • รวมทั้งสอง: library เปิดเผย error ที่มีประเภทผ่าน thiserror, แอปพลิเคชันห่อด้วย anyhow
  • โค้ด async ใช้ pattern เดียวกัน—? ทำงานใน async block โดยไม่ต้องแก้ไข
  • ใช้ ok_or() เพื่อแปลง Option เป็น Result; ใช้ ok() สำหรับกลับกัน
  • Downcast wrapped error เฉพาะเมื่อ logic การกู้คืนเฉพาะต้องการประเภทดั้งเดิม

เริ่มฝึกซ้อมเลย!

ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ

แท็ก

#rust
#error handling
#thiserror
#anyhow

แชร์

บทความที่เกี่ยวข้อง

คู่มือขั้นสูง Rust traits และ generics พร้อมตัวอย่างโค้ดและโลโก้ปู Rust

Rust Traits และ Generics ฉบับสมบูรณ์ 2026: Trait Upcasting, AsyncFn และรูปแบบขั้นสูงสำหรับสัมภาษณ์งาน

คู่มือเชิงลึกเกี่ยวกับ Rust traits และ generics พร้อมฟีเจอร์ใหม่จาก Rust 2024 Edition: trait upcasting, AsyncFn closures, RPITIT และรูปแบบขั้นสูงที่ใช้ในการสัมภาษณ์จริง

แผนภาพการจัดการหน่วยความจำของสมาร์ตพอยน์เตอร์ Rust Box, Rc, Arc และ RefCell

สมาร์ตพอยน์เตอร์ใน Rust อธิบายครบ: Box, Rc, Arc และ RefCell ปี 2026

อธิบายสมาร์ตพอยน์เตอร์ Box, Rc, Arc และ RefCell ใน Rust พร้อมตัวอย่างที่คอมไพล์ได้ปี 2026 ตารางตัดสินใจ และคำถามสัมภาษณ์ที่พบบ่อย

แผนภาพอธิบายการทำงานของ Async/Await ใน Rust ด้วย Tokio Runtime และ Futures

Async/Await ใน Rust: อธิบาย Tokio, Futures และ Asynchronous Concurrency อย่างครบถ้วน

บทความอธิบายการเขียนโปรแกรมแบบ Asynchronous ใน Rust ด้วย async/await, Tokio runtime และ Futures ครอบคลุมตั้งแต่พื้นฐานจนถึงแนวทางปฏิบัติขั้นสูงสำหรับระบบที่ต้องการประสิทธิภาพสูง