Exception behaviour

Exception behaviour

throw/catch/exit와 예외를 다루는 함수들을 제공하는 모듈이에요. 또한 커스텀 예외가 따라야 하는 behaviour도 여기서 정의해요. 나만의 예외를 정의하려면 defexception/1을 참고하면 돼요.

출처: Exception behaviour

본문

포맷팅 함수

이 모듈에는 예외를 포맷해 주는 함수가 여러 개 있어요. 이 중 몇몇 함수는 인자로 **스택트레이스(stacktrace)**를 받아요. 스택트레이스는 보통 catchrescue 안에서 __STACKTRACE__/0 변수를 사용해 얻을 수 있어요.

다만 이 모듈의 함수가 돌려주는 특정 포맷에 의존하지 않는 게 좋아요. 그 포맷은 Elixir의 도구 체인에 더 잘 맞도록 미래 릴리스에서 바뀔 수 있거든요. 다시 말해, 이 모듈의 함수를 쓰면 지금 사용 중인 Elixir 버전의 방식으로 예외를 포맷한다는 것만 보장된다고 생각하면 돼요.

주요 타입

  • kind/0 — 포맷팅 함수가 다루는 종류예요. :error이거나 :exit, :throw, {:EXIT, pid} 같은 비에러 종류예요.
  • stacktrace/0 — 스택트레이스 항목(stacktrace_entry/0)들의 리스트예요.
  • t/0 — 예외 타입이에요.
@type kind() :: :error | non_error_kind()
@type non_error_kind() :: :exit | :throw | {:EXIT, pid()}
@type stacktrace() :: [stacktrace_entry()]
@type stacktrace_entry() ::
  {module(), atom(), arity_or_args(), location()}
  | {(... -> any()), arity_or_args(), location()}
@type t() :: %{
  :__struct__ => module(),
  :__exception__ => term(),
  optional(atom()) => any()
}

콜백

이 behaviour는 세 개의 콜백을 요구해요.

  • exception(term)raise/2에 전달된 인자를 받아 예외 struct를 돌려줘요. 기본 구현은 struct에 병합되는 키워드 인자들의 집합이나, 예외 메시지로 쓸 문자열을 받아들여요.
  • message(t) — 예외 struct를 받아 그 메시지를 돌려줘야 해요. 많은 예외가 message 필드를 갖고 있고 기본적으로 이 함수가 그 필드에 접근해요. 하지만 message 필드가 없는 예외라면 이 함수를 명시적으로 구현해야 해요.
  • blame(t, stacktrace) — (선택) Exception.blame/3에서 예외 struct를 보강하기 위해 호출돼요. 예외에 대한 추가 정보를 모으거나, 비싼 추가 계산을 할 때 쓸 수 있어요.
@callback exception(term()) :: t()
@callback message(t()) :: String.t()
@callback blame(t(), stacktrace()) :: {t(), stacktrace()}

함수들

blame/3 — throw/error/exit에 디버깅을 위한 정보를 붙여줘요. 파일 시스템에서 데이터를 읽고, beam 파일을 파싱하고, 코드를 평가하는 등 비용이 꽤 들 수 있는 연산이에요. kind:error이고 오류가 Erlang 예외라면 정규화하고, Elixir 예외라면 예외 모듈의 선택 콜백인 blame/2를 구현했을 때 호출해요. message/1과 달리 이 함수는 에러를 구조하지 않아요. 콜백이 예외를 던지면 그 에러가 호출자에게 전파돼요.

blame_mfa/3 — 주어진 모듈·함수·인자의 호출을 blame해요. 바이트코드에서 사용 가능한 절들을 가져와 주어진 인자와 대조하고, {args, guards} 쌍의 리스트로 돌려줘요. {:ok, definition, clauses} 또는 :error를 돌려줘요. definition:def, :defp, :defmacro, :defmacrop 중 하나예요.

format/3 — throw/error/exit와 스택트레이스를 정규화하고 포맷해요. 최종 포맷은 format_banner/3format_stacktrace/1에 의존해요. kind{:EXIT, pid}이면 스택트레이스가 없는 메시지로 받아들이기 때문에 스택트레이스를 만들지 않아요.

format_banner/3 — 어떤 throw/error/exit든 정규화하고 포맷해요. Elixir CLI가 쓰는 것과 같은 포맷으로 메시지를 표시해요. 세 번째 인자인 스택트레이스는 정규화된 에러를 더 풍부하게 만들기 위해 쓰이며, kind가 에러일 때만 사용돼요.

format_exit/1 — exit를 포맷해서 문자열로 돌려줘요. exit 안에는 종종 에러/예외가 들어 있고, 호출자에 의해 감싸져 스택트레이스도 제공돼요. 이 함수는 exit 이유, 호출자, 스택트레이스를 잘 보여주도록 포맷해요.

format_fa/2 — 익명 함수와 arity를 받아 스택트레이스처럼 포맷해요. arity는 인자 리스트일 수도 있어요.

Exception.format_fa(fn -> nil end, 1)
#=> "#Function<...>/1"

format_file_line/3 — 파일과 줄 번호를 스택트레이스처럼 포맷해요. 값 중 하나가 nil이면 그 부분은 생략돼요.

iex> Exception.format_file_line("foo", 1)
"foo:1:"

iex> Exception.format_file_line("foo", nil)
"foo:"

format_file_line_column/4 — 파일, 줄, 열을 스택트레이스처럼 포맷해요. nil인 값은 생략돼요.

iex> Exception.format_file_line_column("foo", 1, 2)
"foo:1:2:"

iex> Exception.format_file_line_column("foo", nil, 2)
"foo:"

format_mfa/3 — 모듈, 함수, arity를 받아 스택트레이스처럼 포맷해요. arity는 인자 리스트일 수도 있어요.

iex> Exception.format_mfa(Foo, :bar, 1)
"Foo.bar/1"

iex> Exception.format_mfa(Foo, :bar, [])
"Foo.bar()"

익명 함수는 -func/arity-anonfn-count-로 표시되는데, 여기서 func는 익명 함수를 감싸고 있는 함수의 이름이에요. 이를 "anonymous fn in func/arity"로 변환해요.

format_stacktrace/1 — 스택트레이스를 포맷해요. 인자로 스택트레이스를 주거나, 주지 않으면 Process.info/2에서 가져와요.

format_stacktrace_entry/1 — 스택트레이스 항목 하나를 받아 문자열로 포맷해요.

message/1 — 예외의 메시지를 가져와요. 예외 모듈의 message/1 콜백을 호출해서 메시지를 얻어요. 만약 콜백이 예외를 던지거나 바이너리가 아닌 값을 돌려주면, 이 함수가 그 에러를 구조하고 대신 설명적인 에러 메시지를 돌려줘요.

normalize/3 — 예외를 정규화해서 Erlang 예외를 Elixir 예외로 변환해요. catch가 뱉어낸 종류를 인자로 받아 :error만 정규화하고, 나머지는 변경하지 않은 payload를 돌려줘요. 세 번째 인자인 스택트레이스는 kind가 에러일 때 정규화된 에러를 보강하는 데 쓰여요.

@spec normalize(:error, any(), stacktrace()) :: t()
@spec normalize(non_error_kind(), payload, stacktrace()) :: payload when payload: var

더 알아보기

  • 커스텀 예외를 만드는 방법은 defexception/1 문서를 참고해 보세요.
  • raise/2__STACKTRACE__/0의 동작을 함께 살펴보면 예외 흐름 전체가 이해돼요.
  • 예외 struct의 구조는 Kernel의 예외 관련 특수 폼 문서에서 더 자세히 확인할 수 있어요.