17 try, catch and rescue - try, catch and rescue

Elixir는 3가지(errors, throws and exits)에러 메커니즘을 가지고 있습니다. 이 장에서는 각각에 대해 어떨때 사용하면 좋은지 살펴보겠습니다.

Elixir has three error mechanisms: errors, throws and exits. In this chapter we will explore each of them and include remarks about when each should be used.

17.1 에러 - Errors

원자형에 숫자를 더하려고 하면 샘플오류가 발생합니다:

A sample error can be retrieved by trying to add a number into an atom:

iex> :foo + 1
** (ArithmeticError) bad argument in arithmetic expression
     :erlang.+(:foo, 1)

raise/1매크로를 사용하여 언제든지 런타임 오류를 발생시킬 수 있습니다:

A runtime error can be raised any time by using the raise/1 macro:

iex> raise "oops"
** (RuntimeError) oops

raise/2에 에러이름과 키워드 인자리스트를 넘겨서 다른에러들을 발생시킬 수 있습니다:

Other errors can be raised with raise/2 passing the error name and a list of keyword arguments:

iex> raise ArgumentError, message: "invalid argument foo"
** (ArgumentError) invalid argument foo

자체오류들을 정의하는 모듈을 생성할수도 있고 defexception/1매크로를 집어 넣을수 있습니다. 가장 일반적인 케이스는 메시지 필드와 함께 예외를 정의하는것입니다:

You can also define your own errors by creating a module and use the defexception/1 macro inside it. The most common case is to define an exception with a message field:

iex> defmodule MyError do
iex>  defexception message: "default message"
iex> end
iex> raise MyError
** (MyError) default message
iex> raise MyError, message: "custom message"
** (MyError) custom message

try/rescue구조를 사용하여 예외를 처리할수있습니다:

Exceptions can be rescued by using the try/rescue construct:

iex> try do
...>   raise "oops"
...> rescue
...>   e in RuntimeError -> e
...> end
%RuntimeError{message: "oops"}

위의 예제는 런타임에러를 처리하고 iex세션에서 에러자체를 출력하기 위해 리턴합니다. 그러나 실제로 Elixir를 사용하는 개발자는 try/rescue구조를 사용하는일은 거의 없습니다. 예를들어, 많은 언어에서 파일을 열 수 없을때 에러를 처리하도록 강제하고 있습니다. 한편 Elixir에서는 파일이 성공적으로 오픈됐는지 아닌지를 가지고 있는 튜플을 리턴하는 File.read/1함수를 제공합니다:

The example above rescues the runtime error and returns the error itself which is then printed in the iex session. In practice Elixir developers rarely use the try/rescue construct though. For example, many languages would force you to rescue an error when a file cannot open successfully. Elixir instead provides a File.read/1 function which returns a tuple containing information if the file was opened with success or not:

iex> File.read "hello"
{:error, :enoent}
iex> File.write "hello", "world"
:ok
iex> File.read "hello"
{:ok, "world"}

여기에서는 try/rescue가 없습니다. 파일을 열때 발생하는 여러결과에 대해 처리하고 싶을때는 case를 사용하여 패턴매칭을 하면됩니다:

There is no try/rescue here. In case you want to handle multiple outcomes of opening a file, you can simply use pattern matching with case:

iex> case File.read "hello" do
...>   {:ok, body} -> IO.puts "got ok"
...>   {:error, body} -> IO.puts "got error"
...> end

마지막으로, 파일을 열 때 발생하는 에러의 예외 여부는 어플리케이션에서 어떻게 할지 결정하면 됩니다. 왜냐하면 Elixir는 File.read/1과 다른 많은 함수가 예외를 강요하지 않기 때문입니다. 대신에 어떻게하면 가장 좋을지는 개발자에게 맡겨둡니다.

At the end of the day, it is up to your application to decide if an error while opening a file is exceptional or not. That's why Elixir doesn't impose exceptions on File.read/1 and many other functions. Instead we leave it up to the developer to choose the best way to proceed.

파일의 존재유무만 판단하려면 단순히 File.read!/1을 사용할 수 있습니다:

For the cases where you do expect a file to exist (and the lack of a file is truly an error) you can simply use File.read!/1:

iex> File.read! "unknown"
** (File.Error) could not read file unknown: no such file or directory
    (elixir) lib/file.ex:305: File.read!/1

한마디로 얘기해서 제어흐름을 위해 에러를 사용하지 않기때문에 try/rescue를 사용하지 않습니다. Elixir는 에러를 문자 그대로 취합니다. 하지만 예외적인 상황을 위해서 try/rescue를 사용할수 있습니다. 흐름 제어 구조가 필요한경우에는 throw를 사용해야 합니다. 다음에서 살펴봅시다.

In other words, we avoid using try/rescue because we don't use errors for control flow. In Elixir, we take errors literally: they are reserved to unexpected and/or exceptional situations. In case you actually need flow control constructs, throws must be used. That's what we are going to see next.

17.2 Throws

Elixir에서는 필요한 변수를 throw 할수 있습니다. throwcatch를 사용하지 않으면 값을 찾을할수 없는 경우를 위해 throwcatch를 사용할수 있습니다.

In Elixir, one can throw a value to be caught later. throw and catch are reserved for situations where it is not possible to retrieve a value unless by using throw and catch.

적절한 API를 제공하지 않는 라이브러리와 인터페이스 하지 않는 이상 이러한 상황은 실제로는 매우 드뭅니다. 예를들어 13의 배수가되는 첫번째 숫자를 찾고자 할 때 Enum모듈이 적절한 API를 제공하지 않은 경우를 생각해 봅시다:

Those situations are quite uncommon in practice unless when interfacing with a library that does not provide the proper APIs. For example, let's imagine the Enum module did not provide any API for finding a value and we need to find the first number that is a multiple of 13:

iex> try do
...>   Enum.each -50..50, fn(x) ->
...>     if rem(x, 13) == 0, do: throw(x)
...>   end
...>   "Got nothing"
...> catch
...>   x -> "Got #{x}"
...> end
"Got -39"

그러나 실제로는 Enum.find/2를 사용하는것이 간단합니다:

However, in practice one can simply use Enum.find/2:

iex> Enum.find -50..50, &(rem(&1, 13) == 0)
-39

17.3 Exits

모든 Elixir 코드는 여러 프로세스에서 동작하고 서로 통신하고 있습니다. 프로세스가 죽었을때는 exit 신호를 보냅니다. 프로세스는 명시 적으로 exit 신호를 보냄으로써 죽을 수도 있습니다:

Every Elixir code runs inside processes that communicates with each other. When a process dies, it sends an exit signal. A process can also die by explicitly sending an exit signal:

iex> spawn_link fn -> exit(1) end
#PID<0.56.0>
** (EXIT from #PID<0.56.0>) 1

위의 예제에서는 exit신호를 1과함께 보냄으로써 링크된 프로세스가 죽었습니다. Elixir쉘은 자동으로 이 메시지들을 처리하고 터미널에 출력합니다.

In the example above, the linked process died by sending an exit signal with value of 1. The Elixir shell automatically handles those messages and prints them to the terminal.

exittry/catch로도 catch할 수 있습니다:

exit can also be "caught" using try/catch:

iex> try do
...>   exit "I am exiting"
...> catch
...>   :exit, _ -> "not really"
...> end
"not really"

try/catch를 사용하는 것은 일반적이지 않고 exits을 catch하기 위해 이것을 사용하는것은 훨씬 더 드문일입니다.

Using try/catch is already uncommon and using it to catch exits is even more rare.

exit신호는 Erlang VM에서 제공하는 복원력에 중요한 부분을 담당하고 있습니다. 프로세스들은 일반적으로 감독되고 있는 프로세스들의 exit신호를 기다리는 감독트리(supervision tress)의 프로세스 아래에서 동작합니다. 일단 exit 신호가 오면 supervision strategy가 시작되고 감독되고 있는 프로세스가 재시작됩니다.

exit signals are an important part of the fault tolerant system provided by the Erlang VM. Processes usually run under supervision trees which are themselves processes that just wait for exit signals of the supervised processes. Once an exit signal is received, the supervision strategy kicks in and the supervised process is restarted.

supervision system자체가 try/catchtry/rescue를 닮았기 때문에 Elixir에서는 이 구문들이 별로 사용되지 않는 것입니다. supervision tree가 응용 프로그램의 초기화 상태로 돌려놓는것을 보장하기 때문에 명백한 에러를 rescue하는것보다 "빠른 실패"를 선호합니다.

It is exactly this supervision system that makes constructs like try/catch and try/rescue so uncommon in Elixir. Instead of rescuing a certain error, we'd rather "fail fast" since the supervision tree will guarantee our application will go back to a known initial state after the error.

17.4 After

때로는 특정 동작후에 리소스가 깨끗하게 정리될 수 있도록 try/after가 필요합니다. 예를들어 파일을 열고나서 try/after블록으로 감싸면 파일이 닫히는게 보장됩니다:

Sometimes it is necessary to use try/after to guarantee a resource is cleaned up after some particular action. For example, we can open a file and guarantee it is closed with try/after block:

iex> {:ok, file} = File.open "sample", [:utf8, :write]
iex> try do
...>   IO.write file, "olá"
...>   raise "oops, something went wrong"
...> after
...>   File.close(file)
...> end
** (RuntimeError) oops, something went wrong

17.5 변수 범위 - Variables scope

try/catch/rescue/after블록에서 정의한 변수는 외부에서 보이지 않는다는것을 명심해야 합니다. try블록이 실패 할수도 있고 변수들을 정의한 곳에 바인딩 되지 않기 때문입니다. 한마디로 말해서, 이 코드는 유효하지 않습니다.

It is important to bear in mind that variables defined inside try/catch/rescue/after blocks do not leak to the outer context. This is because the try block may fail and as such the variables may never be bound in the first place. In other words, this code is invalid:

iex> try do
...>   from_try = true
...> after
...>   from_after = true
...> end
iex> from_try
** (RuntimeError) undefined function: from_try/0
iex> from_after
** (RuntimeError) undefined function: from_after/0

이것으로 trycatchrescue에 대한 소개를 마칩니다. Elixir에서는 다른언어들보다 이런것들이 잘 사용되지않는것을 알게되겠지만 규칙을 따르지 않는 라이브러리나 특정 코드에서는 유용할수도 있을것입니다.

This finishes our introduction to try, catch and rescue. You will find they are used less frequently in Elixir than in other languages although they may be handy in some situations where a library or some particular code is not playing "by the rules".

자 이제 Elixir를 구성하고 있는 comprehensions and sigils같은 것들에 대해 이야기 할 시간입니다.

It is time to talk about some Elixir constructs like comprehensions and sigils.