[Rust의 에러 처리, Part 1]에 이어, Rust의 에러 처리에 대해 다뤄보고자 한다.

trait Error

Rust 에러 처리의 두 번째 중추는 Error 트레잇이다.

pub trait Error: Debug + Display {
    fn source(&self) -> Option<&(dyn Error + 'static)> { ... }
}

(deprecated거나 nightly 전용인 메서드가 있지만 여기서는 다루지 않겠다)

fn source(&self)는 이 에러가 발생한 원인을 (존재하면) 반환한다. 공식 문서의 설명을 빌리면, source의 주된 사용처는 ‘추상화의 경계’를 넘나들 때(서로 다른 모듈이라던지) 경계 너머의 에러(즉 지금 반환하려는 에러가 감싸는 다른 모듈에서 유래한 에러)를 표시하는 것이다.

enum/struct Error

필자가 일반적으로 Error 트레이트를 구현하는 방법은 crate 혹은 중요한 기능 단위로서 기능하는 모듈별로 Error 타입을 정의하는 것이다. 각 Error 타입은 그 Error의 근원(source)을 표현해야 하기 때문에 으레 열거형으로 표현하곤 한다.

pub enum Error {
    CannotParseInteger(ParseIntError),
    CannotDeserializeJson(serde_json::Error),
}

이제 에러를 설명하기 위해 impl Display for Error 를 해 주자.

impl Display for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
        match self {
            Self::CannotParseInteger(err) => write!(f, "cannot parse integer"),
            // ...
        }
    }
}

여기서 조금 더 자세하게 설명해보자면,

  • "cannot parse integer"와 같이 에러의 섦여 옆에 근원이 되는 에러를 같이 표시하는 경우도 있다. 필자의 의견으로는, 에러를 ‘예쁘게’ 표시해주는 (후술할)anyhow와 같은 크레이트를 사용하면 Error::source의 연쇄를 자동으로 보여주기 때문에 굳이 이렇게 작성할 필요는 없다고 생각한다.
  • Rust API Guidelines에 따르면, 동사-목적어-Error 순으로 에러 타입의 이름을 정하는 것이 권장된다. 이를 차용하여 열거형의 각 variant들에도 동일한 명명 관습을 적용하도록 하였다.
  • 동일하게, Rust API Guidelines에 따르면, 에러 메시지는 다음을 따를 것을 권장한다.
    • 소문자로 시작해야 한다.
    • 끝에 온점을 두면 안 된다.
    • 간결하게 작성하는 것이 좋다.
  • 이 외에도 에러 ‘타입’에 대해 Send/Sync를 구현할 것을 권장한다거나, Error::description(deprecated)를 구현하지 않을 것을 권한다던가 하는 사항들이 있으니 위 문서를 참고할 것을 권한다.

남은 것은 impl std::error::Error for Error다.

impl std::error::Error for Error {
    fn source(&self) -> Option<&(dyn Error + 'static)>k {
        match self {
            Self::CannotParseInteger(err) => Some(&err)
            // ...
        }
    }
}

(사실 Error::source는 항상 None을 반환하는 default implementation이 존재하지만, 이제 우리는 source를 구현함으로서 이 에러가 어떤 사유로 인해 발생했는지도 추적할 수 있게 된 것이다.)

필자는 enum Error를 주로 작성하지만, 때에 따라서는 에러 타입을 구조체로 정의하는 것이 더 자연스러울 수도 있다. 근원이 되는 에러가 없거나 impl std::error::Error로 표현하기에는 ‘투 머치’인 경우에는 이런 식의 타입을 구성하고는 한다(대표적으로 std::io::Error가 있다):

struct Error {
    kind: ErrorKind,
    backtrace: Backtrace,
}

enum ErrorKind {
    BadParams,
    SyncFailure,
}

그냥 ErrorKindimpl std::error::Error를 하면 되지 않느냐고 반문할 수도 있다. 좋은 지적이다. struct로 에러를 만들 때의 장점은 각각의 경우에 대해 backtrace 같은 공통 속성을 각 varaint마다 일일히 추가할 필요가 없다는 것이다. Rust의 타입 시스템은 열거형과 구조체를 딱히 차별하지 않기 때문에 본인이 원하는 대로 조합해서 쓰는 것이 제일 좋을 것이다.

thiserror

고백하자면, 위에서 거짓말을 하나 했다. 나는 이런 노가다를 Rust 코딩하면서 단 한 번도 해 본적이 없다(정확히는 방금 글 쓰면서 처음 해 봤다). 왜냐면 이 일을 자동으로 해주는 thiserror라는 크레이트가 존재하기 때문이다.

우리의 노가다는 이제 몇 줄의 proc-macro로 대체된다:

use thiserror::Error;

#[derive(Error, Debug)]
enum Error {
    #[error("cannot parse integer")]
    CannotParseInteger(#[source] ParseIntError),
    #[error("cannot deserialize JSON")]
    CannotDeserializeJson(#[source] serde_json::Error),
}

thiserror는 다른 편의 기능들도 제공한다. 첫째로, ?(try) 연산자를 더 편하게 사용하기 위해, #[from]을 필드에 붙여서 impl From<TheFieldType> for Error를 자동으로 구현할 수 있게 도와준다. 이것이 없어도 map_err를 쓰면 손쉽게 (명시적인) 변환이 가능하기 때문에, 꼭 붙여야 할 필요는 없다.

// 위 예시를 그대로 사용했다면
let num: i32 = some_string.parse().map_err(Error::CannotParseInteger)?;
// 위 예시에서 #[source] 자리에 #[from]을 붙였다면
let num: i32 = some_string.parse()?;

참고로 #[from]#[source]를 암시하기 때문에 둘을 동시에 사용할 일은 없다.

필자의 개인적인 의견으로 #[from]은 주의해서 사용해야 하는데, 첫 번째 이유로는 From<InnerError>을 구현하게 되면 반환할 수 있는 배리언트가 하나로 고정되어 버린다는 문제가 있기 때문이다.(위 예시에서 CannotSerializeJson(serde_json::Error)가 추가된다면 From<serde_json::Error>는 어떻게 구현되어야 할까?)

튜플 열거형이 아닌 구조체 형식의 열거형이나 구조체를 사용할 경우에는, source 라는 이름의 필드를 추가하는 것으로 #[source]의 효과를 자동으로 누릴 수 있으니 참고하기 바란다.

단순히 여러 타입의 에러를 감싸기만 하는 용도(즉 sourceDisplay 구현을 따로 감싸고 싶지 않은 경우)에는 #[transparent]를 사용할 수 있다.

#[derive(Error, Debug)]
pub enum MyError {
    ...

    #[error(transparent)]
    Other(#[from] anyhow::Error),  // source and Display delegate to anyhow::Error
}

(thiserror의 문서에서 발췌)

Box<dyn Error + 'static>

모듈의 경우에는 코드를 작성하는 시점에 발생할 오류를 모두 알고 있지만, 그 경우의 수가 너무 방대하거나 컴파일 시점에 알기 어려운 경우에는 위처럼 열거형으로 오류의 가짓수를 표현하는 것이 어려울 것이다. 이 때는 트레잇 객체를 사용해보자.

dyn Error (+ Send) (+ Sync) + 'static은 에러 타입의 다운캐스팅(언박싱?)을 지원한다:

  • fn downcast<T>(Box<dyn Error + 'static>) -> Result<Box<T>, Box<dyn Error + 'static>>
  • fn downcast_ref<T>(&self) -> Option<&T>
  • fn downcast_mut<T>(&mut self) -> Option<&mut T>

(기대한 타입 T와 실제 타입이 맞지 않으면 ErrNone을 반환하니, 아마 원하는 타입들에 대해 match 또는 if let Some(err) = … {} `을 연쇄적으로 사용하게 될 것이다)

이는 런타임에 여러 종류의 에러를 한 타입으로 합치고 다시 분리하는 것을 가능케 한다.

anyhow & eyre

thiserror가 정적인 enum Error의 편리한 버전이라면, anyhow는 동적인 Box<dyn Error + 'static>의 편리한 버전이다.

Box<dyn Error + 'static> 대신에, anyhowanyhow::Error를 제공한다. Box<dyn Error + 'static>의 기능들(다운캐스팅 등)을 지원하면서 그보다 편리한 점이 몇 개 있는데,

  • anyhow::Error는 임의의 std::error::Error + Send + Sync + 'static으로부터 변환 가능하기에(From 트레잇), Result<_, anyhow::Error>를 반환하는 함수 안에서 사용하는 ? 연산자는 추가적인 .map_err가 필요 없다(엄밀히 말해서는 해당 에러가 std::error::Error를 구현하지 않는 경우도 있으나 드문 경우이므로 논외로 치자).
  • anyhow::Error::contextanyhow::Error에 추가적인 에러 발생의 맥락을 저장할 수 있게 한다. 모든 상황에 대해서 에러 타입을 만들기보다는 공통된 상황으로 카테고리를 묶고(열거형 등으로) 해당 에러가 발생하는 지점마다 맥락을 제공하면 더 효율적인 에러 처리가 가능해진다. (이렇게 추가된 맥락은 anyhow::ErrorDebug 표현에서 확인하거나 다운캐스팅을 통해 꺼내올 수 있다.)
#[derive(Error, Debug)]
enum Error {
    CannotSendData(std::io::Error),
    CannotReceiveData(std::io::Error),
}

// send()와 recv()는 Result<_, std::io::Error>를 반환
fn do() -> anyhow::Result<()> {
    send().map_err(Error::CannotSendData)?;
    let r = recv().map_err(Error::CannotReceiveData)?;
}
// send()와 recv()는 Result<_, std::io::Error>를 반환
// 참고: anyhow::Result<T, E = Error> = Result<T, E>;
// 참고: 사실 anyhow::Error::context가 아니라 Context 트레잇의 .context() 메서드를 쓰고 있다.
fn do() -> anyhow::Result<()> {
    send().context("cannot send data")?;
    let r = recv().context("cannot receive data")?;
}

(물론 이렇게 맥락에 문자열을 넣으면 나중에 맥락을 꺼내 처리할 때 문자열 비교를 동반하기 때문에, 에러 처리를 상황별로 적절하게 해야 하는 경우에는 열거형을 넣거나 하는 것이 좋을 것으로 생각된다.)

이 외에도 anyhow는 몇 가지 편리한 기능을 추가적으로 제공한다:

  • anyhow! 매크로: 서식 문자열과 파라미터가 주어지면, format!과 비슷하지만 문자열 대신 해당 문자열을 내용으로 가지는 에러를 생성한다. Debug + Display(혹은 더 나아가 Error)를 구현하는 단일 값을 제공할 수도 있다.
  • bail! 매크로: anyhow! 매크로와 같지만 생성된 에러를 즉시 반환한다. panic!의 에러 핸들링 버전으로 보아도 무방하다.
  • ensure! 매크로: bail! 매크로가 panic!에 대응된다면, 이 매크로는 assert!에 대응된다. 첫 번째 파라미터로 주어진 조건문이 false면 bail!한다.

eyre 크레이트는 anyhow의 포크로, logtracing과 비슷하게 에러의 생성/표시 방식을 사용자가 커스텀할 수 있게 한다. 대표적인 에러 표시 방식 지정 크레이트인 color-eyre에 대해 알아보자.

color-eyre

eyre 크레이트에 주로 붙이는 크레이트인 color-eyreeyre::Report(anyhow::Error에 대응)를 아주 멋드러지게 표현해준다.

❯ cargo run --example custom_section
    Finished dev [unoptimized + debuginfo] target(s) in 0.04s
     Running `target/debug/examples/custom_section`
Error:
   0: Unable to read config
   1: cmd exited with non-zero status code

Stderr:
   cat: fake_file: No such file or directory

  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ SPANTRACE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

   0: custom_section::output2 with self="cat" "fake_file"
      at examples/custom_section.rs:14
   1: custom_section::read_file with path="fake_file"
      at examples/custom_section.rs:58
   2: custom_section::read_config
      at examples/custom_section.rs:63

Suggestion: try using a file that exists next time

이 외에도 color-eyre는 다음의 편의 기능들을 가진다:

  • backtrace-rs 지원: Stable Rust에서도 backtrace를 사용할 수 있게 한다.

    backtrace-rs는 외부 크레이트이므로 표준 라이브러리와 다르게 최적화가 되지 않아, 디버그 빌드에서는 성능 손해가 커질 수 있다. color-eyre의 문서에 따르면, 아래의 프로필을 Cargo.toml에 붙이는 것이 권장된다:

[profile.dev.package.backtrace]
opt-level = 3
  • 커스텀 섹션: .section() 메서드를 사용해서 에러에 원하는 섹션을 추가하거나(일종의 디버그 출력 용도의 anyhow::Context라고 보면 되겠다), .suggestion() 메서드로 권장 사항을 표시할 수도 있다.
  • Spantrace 출력: tracing을 사용중이라면 지금 실행 맥락에서의 span을 거슬러 올라가 어떤 연유로 이 코드 지점까지 도달했는지를 알 수 있게 된다. Spantrace는 backtrace와 다르게 함수 파라미터 등도 같이 저장할 수 있고, backtrace보다 더 캡쳐에 드는 리소스 소모가 적다.
  • 더 화려한 에러 출력: RUST_LIB_BACKTRACE 환경 변수를 full로 주면, 소스코드의 줄 번호와 주변의 소스코드 내용을(읽을 수 있다면) 표시해준다:
❯ RUST_LIB_BACKTRACE=full cargo run --example usage
    Finished dev [unoptimized + debuginfo] target(s) in 0.05s
     Running `target/debug/examples/usage`
Jul 05 19:16:06.335  INFO read_config:read_file{path="fake_file"}: Reading file
Error:
   0: Unable to read config
   1: No such file or directory (os error 2)

  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ SPANTRACE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

   0: usage::read_file with path="fake_file"
      at examples/usage.rs:32
        30 │ }
        31 │
        32 > #[instrument]
        33 │ fn read_file(path: &str) -> Result<(), Report> {
        34 │     info!("Reading file");
   1: usage::read_config
      at examples/usage.rs:38
        36 │ }
        37 │
        38 > #[instrument]
        39 │ fn read_config() -> Result<(), Report> {
        40 │     read_file("fake_file")

  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ BACKTRACE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
                                ⋮ 5 frames hidden ⋮                               
   6: usage::read_file::haee210cb22460af3
      at /home/jlusby/git/yaahc/color-eyre/examples/usage.rs:35
        33 │ fn read_file(path: &str) -> Result<(), Report> {
        34 │     info!("Reading file");
        35 >     Ok(std::fs::read_to_string(path).map(drop)?)
        36 │ }
        37 │
   7: usage::read_config::ha649ef4ec333524d
      at /home/jlusby/git/yaahc/color-eyre/examples/usage.rs:40
        38 │ #[instrument]
        39 │ fn read_config() -> Result<(), Report> {
        40 >     read_file("fake_file")
        41 │         .wrap_err("Unable to read config")
        42 │         .suggestion("try using a file that exists next time")
   8: usage::main::hbe443b50eac38236
      at /home/jlusby/git/yaahc/color-eyre/examples/usage.rs:11
         9 │     color_eyre::install()?;
        10 │
        11 >     Ok(read_config()?)
        12 │ }
        13 │
                                ⋮ 10 frames hidden ⋮                              

Suggestion: try using a file that exists next time

멋지지 않은가?

thiserror vs anyhow/eyre

두 에러 처리 ‘방식’ (혹은 ‘철학?’)의 가장 큰 차이점은, thiserror의 경우 열거형이나 구조체를 통해 정의하므로 에러의 종류나 내용이 타입으로서 드러나는 데에 비해, anyhow의 경우 박싱을 통해 에러가 어떻게 ‘생겨먹었는지’를 숨긴다는 것이다. 실제로 anyhow::Error가 담고 있는 타입을 꺼내오려면 자신이 생각하는 타입으로 다운캐스트해보는 수 밖에 없다.

그래서, anyhow레포지토리 README를 보면, 라이브러리의 경우는 thiserror를 권장하고(라이브러리 사용자가 에러의 내용을 보고 각각 다른 로직으로 처리할 여지를 줄 수 있도록), 라이브러리들을 마지막에 사용하는 ’end-user’ 크레이트(뭉뚱그려 말해 main.rs가 있는 크레이트)의 경우 anyhow(혹은 eyre)를 사용할 것을 권장하고 있다.


부록 느낌으로, 1부에서 너무 간략하게 다루고 넘어가버린 panic!에 대해 조금만 더 설명해 보고자 한다.

panic! & catch_unwind

panic!이 예외를 던지는, 다른 언어의 throwraise에 대응된다면, try-catch에 대응되는 것이 바로 std::panic::catch_unwind다. 이전 글에서 언급했듯, panic-catch를 다른 언어의 try-catch와 같이 사용하는 것은 기피해야 하지만, catch_unwind가 필요한 경우도 존재하기는 한다.

‘길게 실행되어야 하는’ 메인 로직이 존재하여 매 요청마다 서브 로직을 수행하는 패턴을 사용하는 경우, 서브 로직이 실패하여 panic이 발생하면 메인 로직도 같이 panic에 휘말려 프로세스 전체가 종료될 것이다. 이를 방지하기 위해 메인 로직이 서브 로직을 실행할 때 catch_unwind로 감싸서 패닉을 처리 수 있다. 다시 말해, panic은 주로 처리할 수 없는 에러를 만났을 때 발생하므로 catch_unwind를 통해 복구할 수 있는 여지는 없지만, 서브 로직의 실패가 메인 로직의 실패로 이어지지 않도록 격리한다는 의미이다. 이러한 패턴은 주로 웹 서버 등에서 발견되고는 한다.

  • 이와 별개로, Rust의 panic 매커니즘은 panic이 발생하지 않는(정확히는 unwind하지 않는) 상황에 극도로 최적화되어 있기 때문에, 다른 언어의 exception에 비해 그 비용이 비싸다. 이는 panic-catch를 주된 에러 처리 매커니즘으로 사용하면 안 되는 이유 중에 하나이기도 하다.

Rust’s current unwinding implementation is heavily optimized for the “doesn’t unwind” case. 1

std::panic::set_hook

‘panic hook’을 설정한다. panic hook은 패닉을 만났을 때 실행되는 함수인데, 기본적으로는 패닉 메시지를 stderr에 표시하고 (RUST_BACKTRACE 환경 변수가 설정된 경우) backtrace도 함께 표시해주는 hook이 설치되어 있다. 만약 자신이 (Sentry와 같이) 에러 추적 서비스를 만들고 있거나 할 때 사용할 일이 있을 것이다.

Unwind Safety

기본 설정 하에서는, panic이 발생하면 해당 프로그램은 (catch_unwind를 만날 때까지) 콜 스택을 거슬러 올라가면서 실행 중인 맥락을 끊고 ‘탈출’(즉시 반환)하는데, 이를 unwinding이라 한다. 이는 일부 경우에 예상하지 못한 결과를 낳을 수 있는데, 이러한 코드를 생각해 보자.

/// Invariant: both hands are `Some`
struct Juggler<Func: FnMut()> {
    left_hand: Option<i32>,
    right_hand: Option<i32>,
    func: Func,
}

impl<Func: FnMut()> Juggler<Func> {
    fn juggle(&mut self) {
        let tmp = self.left_hand.take();
        (self.func)();
        self.left_hand = self.right_hand;
        self.right_hand = tmp;
    }

    fn print(&self) {
        println!("left: {:?}, right: {:?}", self.left_hand, self.right_hand);
    }
}

fn main() {
    let mut cnt = 0;
    let mut juggler = Juggler {
        left_hand: Some(3),
        right_hand: Some(1),
        func: || {
            cnt += 1;
            if cnt > 3 {
                panic!("tired");
            }
        }
    };

    for _ in 0..5 {
        juggler.print();
        juggler.juggle();
    }
}

(std::mem::replace를 쓰면 되지만 일단 무시하자)

.juggle() 메서드는 양손에 든 것을 뒤바꾸는데, 왼손의 것을 공중에 올린 뒤에 무언가(self.func)를 하고 오른손에 안착시킨다. 그런데 저글러가 만약 공중에 무언가를 올린 채로 panic을 내 버리면 어떻게 될까? 공중에 올려진 값은 지역 변수이므로 즉시 반환하면서 소실될 것이다. 프로그램 전체가 즉시 종료된다면 소실 여부가 중요하지 않지만, catch_unwind를 사용한다면…

    for _ in 0..5 {
        juggler.print();
        catch_unwind(|| juggler.juggle());
    }
error[E0277]: the type `&mut Juggler<[closure@src/main.rs:27:15: 27:17]>` may not be safely transferred across an unwind boundary
  --> src/main.rs:37:22
   |
37 |         catch_unwind(|| juggler.juggle());
   |         ------------ --^^^^^^^^^^^^^^^^^
   |         |            |
   |         |            `&mut Juggler<[closure@src/main.rs:27:15: 27:17]>` may not be safely transferred across an unwind boundary
   |         |            within this `[closure@src/main.rs:37:22: 37:24]`
   |         required by a bound introduced by this call
   |
   = help: within `[closure@src/main.rs:37:22: 37:24]`, the trait `UnwindSafe` is not implemented for `&mut Juggler<[closure@src/main.rs:27:15: 27:17]>`
   = note: `UnwindSafe` is implemented for `&Juggler<[closure@src/main.rs:27:15: 27:17]>`, but not for `&mut Juggler<[closure@src/main.rs:27:15: 27:17]>`
note: required because it's used within this closure
  --> src/main.rs:37:22
   |
37 |         catch_unwind(|| juggler.juggle());
   |                      ^^
note: required by a bound in `catch_unwind`
  --> /rustc/fc594f15669680fa70d255faec3ca3fb507c3405/library/std/src/panic.rs:136:40
   |
   = note: required by this bound in `catch_unwind`

컴파일이 되지 않는다! 이는 기본적으로 juggler.juggle()을 수행하던 중 panic이 발생하면, 우리는 앞서 말했듯이 ‘망한’(양손 중 하나가 None인) Juggler를 갖게 되기 때문이다. catch_unwind를 쓰면 이 ‘망해버린’ 상태를 관측할 수 있고 이는 로직 버그로 이어지기 때문에 타입 시스템을 이용해 catch_unwind에 들어가는 값은 UnwindSafe를 구현해야 한다는 제약을 걸게 된다. 다시 말해, &mut Juggler를 담고(정확히는 캡쳐하고) 있는 클로저 || juggler.juggle()은 unwind safe하지 않다.

강제로 || juggler.juggle() 클로저를 unwind safe하다고 컴파일러에게 명시하고 싶으면 AssertUnwindSafe로 감싸면 된다:

    for _ in 0..5 {
        juggler.print();
        catch_unwind(AssertUnwindSafe(|| juggler.juggle()));
    }
Standard Error

   Compiling playground v0.0.1 (/playground)
warning: unused `Result` that must be used
  --> src/main.rs:38:9
   |
38 |         catch_unwind(AssertUnwindSafe(|| juggler.juggle()));
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = note: this `Result` may be an `Err` variant, which should be handled
   = note: `#[warn(unused_must_use)]` on by default

warning: `playground` (bin "playground") generated 1 warning
    Finished dev [unoptimized + debuginfo] target(s) in 0.64s
     Running `target/debug/playground`
thread 'main' panicked at 'tired', src/main.rs:31:17
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
thread 'main' panicked at 'tired', src/main.rs:31:17

Standard Output

left: Some(3), right: Some(1)
left: Some(1), right: Some(3)
left: Some(3), right: Some(1)
left: Some(1), right: Some(3)
left: None, right: Some(3)

대신에 이제 우리는 저글러가 한쪽 값을 잃어버린, ‘망한’ 상태를 관측할 수 있게 된다. Safe Rust에서는 이것이 undefined behavior로 이어지지는 않고 로직 버그나 panic으로 이어질 것이다. 그러나 unsafe 코드라면 이야기가 달라지는데, 만약 위 예시에서 저글러의 손에 있는 값을 꺼내올 때 unwrap_unchecked를 사용했다면 Safe Rust만으로(catch_unwind도 safe한 함수이다) undefined behavior를 유발하는 soundness hole을 만든 셈이 되므로 unsafe Rust에서는 이러한 panic의 발생 가능 지점과 unwind safety에 대해서도 특별히 주의를 기울여야 함을 알 수 있다.

panic = "abort"

Unwinding이 불가능한 플랫폼이거나, unwinding을 구현하기 위해 수반되는 코드가 바이너리에 들어가는 것이 싫은 경우 Cargo.toml의 프로필에 panic = "abort"를 설정할 수 있다. 이 경우에는 위에서 설명한 unwinding 관련 매커니즘이 전혀 동작하지 않고, panic을 만나면 그 지점에서 즉시 프로그램을 종료한다. 이는 panic-catch를 주된 에러 처리 매커니즘으로 사용할 수 없는 이유이기도 하다.

(panic = "abort"가 아니더라도, 일부 panic은 무조건 panic = "abort"인 것처럼 동작한다. 대표적인 예시로 메모리가 부족하여 할당에 실패하는 경우가 있다.)