Skip to content

Commit f231d86

Browse files
committed
Add koans about error-handling
1 parent d1ced0c commit f231d86

File tree

2 files changed

+237
-0
lines changed

2 files changed

+237
-0
lines changed

lib/koans/22_error_handling.ex

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
defmodule ErrorHandling do
2+
@moduledoc false
3+
use Koans
4+
5+
@intro "Error Handling - Dealing gracefully with things that go wrong"
6+
7+
koan "Result tuples are a common pattern for success and failure" do
8+
parse_number = fn string ->
9+
case Integer.parse(string) do
10+
{number, ""} -> {:ok, number}
11+
_ -> {:error, :invalid_format}
12+
end
13+
end
14+
15+
assert parse_number.("123") == ___
16+
assert parse_number.("abc") == ___
17+
end
18+
19+
koan "Pattern matching makes error handling elegant" do
20+
divide = fn x, y ->
21+
case y do
22+
0 -> {:error, :division_by_zero}
23+
_ -> {:ok, x / y}
24+
end
25+
end
26+
27+
result =
28+
case divide.(10, 2) do
29+
{:ok, value} -> "Result: #{value}"
30+
{:error, reason} -> "Error: #{reason}"
31+
end
32+
33+
assert result == ___
34+
end
35+
36+
koan "Try-rescue catches runtime exceptions" do
37+
result =
38+
try do
39+
10 / 0
40+
rescue
41+
ArithmeticError -> "Cannot divide by zero!"
42+
end
43+
44+
assert result == ___
45+
end
46+
47+
koan "Try-rescue can catch specific exception types" do
48+
safe_list_access = fn list, index ->
49+
try do
50+
{:ok, Enum.at(list, index)}
51+
rescue
52+
FunctionClauseError -> {:error, :invalid_argument}
53+
e in Protocol.UndefinedError -> {:error, "#{e.value} is not a list"}
54+
end
55+
end
56+
57+
assert safe_list_access.([1, 2, 3], 1) == ___
58+
assert safe_list_access.([1, 2, 3], "a") == ___
59+
assert safe_list_access.("abc", 0) == ___
60+
end
61+
62+
koan "Multiple rescue clauses handle different exceptions" do
63+
risky_operation = fn input ->
64+
try do
65+
case input do
66+
"divide" -> 10 / 0
67+
"access" -> Map.fetch!(%{}, :missing_key)
68+
"convert" -> String.to_integer("not_a_number")
69+
_ -> {:ok, "success"}
70+
end
71+
rescue
72+
ArithmeticError -> {:error, :arithmetic}
73+
KeyError -> {:error, :missing_key}
74+
ArgumentError -> {:error, :invalid_argument}
75+
end
76+
end
77+
78+
assert risky_operation.("divide") == ___
79+
assert risky_operation.("access") == ___
80+
assert risky_operation.("convert") == ___
81+
assert risky_operation.("safe") == ___
82+
end
83+
84+
koan "Try-catch handles thrown values" do
85+
result =
86+
try do
87+
throw(:early_return)
88+
"this won't be reached"
89+
catch
90+
:early_return -> "caught thrown value"
91+
end
92+
93+
assert result == ___
94+
end
95+
96+
koan "After clause always executes for cleanup" do
97+
cleanup_called =
98+
try do
99+
raise "something went wrong"
100+
rescue
101+
RuntimeError -> :returned_value
102+
after
103+
IO.puts("Executed but not returned")
104+
end
105+
106+
assert cleanup_called == ___
107+
end
108+
109+
koan "After executes even when there's no error" do
110+
{result, value} =
111+
try do
112+
{:success, "it worked"}
113+
after
114+
IO.puts("Executed but not returned")
115+
end
116+
117+
assert result == ___
118+
assert value == ___
119+
end
120+
121+
defmodule CustomError do
122+
defexception message: "something custom went wrong"
123+
end
124+
125+
koan "Custom exceptions can be defined and raised" do
126+
result =
127+
try do
128+
raise CustomError, message: "custom failure"
129+
rescue
130+
e in CustomError -> "caught custom error: #{e.message}"
131+
end
132+
133+
assert result == ___
134+
end
135+
136+
koan "Bang functions raise exceptions on failure" do
137+
result =
138+
try do
139+
Map.fetch!(%{a: 1}, :b)
140+
rescue
141+
KeyError -> "key not found"
142+
end
143+
144+
assert result == ___
145+
end
146+
147+
koan "Exit signals can be caught and handled" do
148+
result =
149+
try do
150+
exit(:normal)
151+
catch
152+
:exit, :normal -> "caught normal exit"
153+
end
154+
155+
assert result == ___
156+
end
157+
158+
koan "Multiple clauses can handle different error patterns" do
159+
handle_database_operation = fn operation ->
160+
try do
161+
case operation do
162+
:connection_error -> raise "connection failed"
163+
:timeout -> exit(:timeout)
164+
:invalid_query -> throw(:bad_query)
165+
:success -> {:ok, "data retrieved"}
166+
end
167+
rescue
168+
e in RuntimeError -> {:error, {:exception, e.message}}
169+
catch
170+
:exit, :timeout -> {:error, :timeout}
171+
:bad_query -> {:error, :invalid_query}
172+
end
173+
end
174+
175+
assert handle_database_operation.(:connection_error) == ___
176+
assert handle_database_operation.(:timeout) == ___
177+
assert handle_database_operation.(:invalid_query) == ___
178+
assert handle_database_operation.(:success) == ___
179+
end
180+
181+
koan "Error information can be preserved and enriched" do
182+
enriched_error = fn ->
183+
try do
184+
String.to_integer("not a number")
185+
rescue
186+
e in ArgumentError ->
187+
{:error,
188+
%{
189+
type: :conversion_error,
190+
original: e,
191+
context: "user input processing",
192+
message: "Failed to convert string to integer"
193+
}}
194+
end
195+
end
196+
197+
{:error, error_info} = enriched_error.()
198+
assert error_info.type == ___
199+
assert error_info.context == ___
200+
end
201+
end
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
defmodule ErrorHandlingTests do
2+
use ExUnit.Case
3+
import TestHarness
4+
5+
test "Error Handling" do
6+
answers = [
7+
{:multiple, [{:ok, 123}, {:error, :invalid_format}]},
8+
"Result: 5.0",
9+
"Cannot divide by zero!",
10+
{:multiple, [{:ok, 2}, {:error, :invalid_argument}, {:error, "abc is not a list"}]},
11+
{:multiple,
12+
[
13+
{:error, :arithmetic},
14+
{:error, :missing_key},
15+
{:error, :invalid_argument},
16+
{:ok, "success"}
17+
]},
18+
"caught thrown value",
19+
:returned_value,
20+
{:multiple, [:success, "it worked"]},
21+
"caught custom error: custom failure",
22+
"key not found",
23+
"caught normal exit",
24+
{:multiple,
25+
[
26+
{:error, {:exception, "connection failed"}},
27+
{:error, :timeout},
28+
{:error, :invalid_query},
29+
{:ok, "data retrieved"}
30+
]},
31+
{:multiple, [:conversion_error, "user input processing"]}
32+
]
33+
34+
test_all(ErrorHandling, answers)
35+
end
36+
end

0 commit comments

Comments
 (0)