Skip to content

Commit bdde15d

Browse files
committed
Revamp try/catch guide, closes elixir-lang#1526
1 parent d738a43 commit bdde15d

File tree

1 file changed

+42
-13
lines changed

1 file changed

+42
-13
lines changed

getting-started/try-catch-and-rescue.markdown

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Elixir has three error mechanisms: errors, throws, and exits. In this chapter, w
88

99
## Errors
1010

11-
Errors (or *exceptions*) are used when exceptional things happen in the code. A sample error can be retrieved by trying to add a number into an atom:
11+
Errors (or *exceptions*) are used when exceptional things happen in the code. A sample error can be retrieved by trying to add a number to an atom:
1212

1313
```elixir
1414
iex> :foo + 1
@@ -30,7 +30,7 @@ iex> raise ArgumentError, message: "invalid argument foo"
3030
** (ArgumentError) invalid argument foo
3131
```
3232

33-
You can also define your own errors by creating a module and using the `defexception` construct inside it; this way, you'll create an error with the same name as the module it's defined in. The most common case is to define a custom exception with a message field:
33+
You can also define your own errors by creating a module and using the `defexception` construct inside it. This way, you'll create an error with the same name as the module it's defined in. The most common case is to define a custom exception with a message field:
3434

3535
```elixir
3636
iex> defmodule MyError do
@@ -53,9 +53,9 @@ iex> try do
5353
%RuntimeError{message: "oops"}
5454
```
5555

56-
The example above rescues the runtime error and returns the error itself which is then printed in the `iex` session.
56+
The example above rescues the runtime error and returns the exception itself, which is then printed in the `iex` session.
5757

58-
If you don't have any use for the error, you don't have to provide it:
58+
If you don't have any use for the exception, you don't have to pass a variable to `rescue`:
5959

6060
```elixir
6161
iex> try do
@@ -66,7 +66,7 @@ iex> try do
6666
"Error!"
6767
```
6868

69-
In practice, however, Elixir developers rarely use the `try/rescue` construct. For example, many languages would force you to rescue an error when a file cannot be opened successfully. Elixir instead provides a `File.read/1` function which returns a tuple containing information about whether the file was opened successfully:
69+
In practice, Elixir developers rarely use the `try/rescue` construct. For example, many languages would force you to rescue an error when a file cannot be opened successfully. Elixir instead provides a `File.read/1` function which returns a tuple containing information about whether the file was opened successfully:
7070

7171
```elixir
7272
iex> File.read("hello")
@@ -77,7 +77,7 @@ iex> File.read("hello")
7777
{:ok, "world"}
7878
```
7979

80-
There is no `try/rescue` here. In case you want to handle multiple outcomes of opening a file, you can use pattern matching within the `case` construct:
80+
There is no `try/rescue` here. In case you want to handle multiple outcomes of opening a file, you can use pattern matching using the `case` construct:
8181

8282
```elixir
8383
iex> case File.read("hello") do
@@ -86,8 +86,6 @@ iex> case File.read("hello") do
8686
...> end
8787
```
8888

89-
At the end of the day, it's 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, it leaves it up to the developer to choose the best way to proceed.
90-
9189
For the cases where you do expect a file to exist (and the lack of that file is truly an *error*) you may use `File.read!/1`:
9290

9391
```elixir
@@ -96,9 +94,27 @@ iex> File.read!("unknown")
9694
(elixir) lib/file.ex:272: File.read!/1
9795
```
9896

99-
Many functions in the standard library follow the pattern of having a counterpart that raises an exception instead of returning tuples to match against. The convention is to create a function (`foo`) which returns `{:ok, result}` or `{:error, reason}` tuples and another function (`foo!`, same name but with a trailing `!`) that takes the same arguments as `foo` but which raises an exception if there's an error. `foo!` should return the result (not wrapped in a tuple) if everything goes fine. The [`File` module](https://hexdocs.pm/elixir/File.html) is a good example of this convention.
97+
At the end of the day, it's 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, it leaves it up to the developer to choose the best way to proceed.
98+
99+
Many functions in the standard library follow the pattern of having a counterpart that raises an exception instead of returning tuples to match against. The convention is to create a function (`foo`) which returns `{:ok, result}` or `{:error, reason}` tuples and another function (`foo!`, same name but with a trailing `!`) that takes the same arguments as `foo` but which raises an exception if there's an error. `foo!` should return the result (not wrapped in a tuple) if everything goes fine.
100+
101+
### Reraise
100102

101-
In Elixir, we avoid using `try/rescue` because **we don't use errors for control flow**. We take errors literally: they are reserved for unexpected and/or exceptional situations. In case you actually need flow control constructs, *throws* should be used. That's what we are going to see next.
103+
While we generally avoid using `try/rescue` in Elixir, one situation where we may want to use such constracuts is for observability/monitoring. Imagine you want to log that something went wrong, you could do:
104+
105+
```elixir
106+
try do
107+
... some code ...
108+
rescue
109+
e ->
110+
Logger.error(Exception.format(:error, e, __STACKTRACE__))
111+
reraise e, __STACKTRACE__
112+
end
113+
```
114+
115+
In the example above, we rescued the exception, logged it, and then re-raised it. We use the `__STACKTRACE__` construct both when formatting the exception and when re-raising. This ensures we reraise the exception as is, without changing value or its origin.
116+
117+
Generally speaking, we take errors in Elixir literally: they are reserved for unexpected and/or exceptional situations, never for controlling the flow of our code. In case you actually need flow control constructs, *throws* should be used. That's what we are going to see next.
102118

103119
## Throws
104120

@@ -214,7 +230,7 @@ Exceptions in the `else` block are not caught. If no pattern inside the `else` b
214230

215231
## Variables scope
216232

217-
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:
233+
Similar to `case`, `cond`, `if` and other constructs in Elixir, variables defined inside `try/catch/rescue/after` blocks do not leak to the outer context. In other words, this code is invalid:
218234

219235
```elixir
220236
iex> try do
@@ -227,7 +243,7 @@ iex> what_happened
227243
** (RuntimeError) undefined function: what_happened/0
228244
```
229245

230-
Instead, you can store the value of the `try` expression:
246+
Instead, you should return the value of the `try` expression:
231247

232248
```elixir
233249
iex> what_happened =
@@ -241,4 +257,17 @@ iex> what_happened
241257
:rescued
242258
```
243259

244-
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".
260+
Furthermore, variables defined in the do-block of `try` are not available inside `rescue/after/else` either. This is because the `try` block may fail at any moment and therefore the variables may have never been bound in the first place. So this also isn't valid:
261+
262+
```elixir
263+
iex> try do
264+
...> raise "fail"
265+
...> what_happened = :did_not_raise
266+
...> rescue
267+
...> _ -> what_happened
268+
...> end
269+
iex> what_happened
270+
** (RuntimeError) undefined function: what_happened/0
271+
```
272+
273+
This finishes our introduction to `try`, `catch`, and `rescue`. You will find they are used less frequently in Elixir than in other languages.

0 commit comments

Comments
 (0)