Skip to content

Commit 251413a

Browse files
committed
Adopt EEP 54
With EEP 54 (part of Erlang/OTP 24+), we can now get extra information from Erlang errors. Before this patch: Interactive Elixir (1.12.0-dev) iex(1)> ets = :ets.new(:example, []) #Reference<0.3845811859.2669281281.223553> iex(2)> :ets.delete(ets) true iex(3)> :ets.insert(ets, :should_be_a_tuple) ** (ArgumentError) argument error (stdlib 3.15) :ets.insert(#Reference<0.3845811859.2669281281.223553>, :should_be_a_tuple) After this patch: Interactive Elixir (1.12.0-dev) iex(1)> ets = :ets.new(:example, []) #Reference<0.105641012.1058144260.76455> iex(2)> :ets.delete(ets) true iex(3)> :ets.insert(ets, :should_be_a_tuple) ** (ArgumentError) errors were found at the given arguments: * 1st argument: the table identifier does not refer to an existing ETS table * 2nd argument: not a tuple (stdlib 3.15) :ets.insert(#Reference<0.105641012.1058144260.76455>, :should_be_a_tuple) We choose to always track error info, instead of moving this to blame, because it should be relatively cheap and it is essential, especially when debugging live systems.
1 parent 3eb3a2e commit 251413a

File tree

9 files changed

+115
-46
lines changed

9 files changed

+115
-46
lines changed

lib/elixir/lib/exception.ex

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -709,7 +709,7 @@ defmodule ArgumentError do
709709

710710
@impl true
711711
def blame(
712-
%{message: "argument error"} = exception,
712+
exception,
713713
[{:erlang, :apply, [module, function, args], _} | _] = stacktrace
714714
) do
715715
message =
@@ -783,12 +783,7 @@ defmodule ArithmeticError do
783783
end
784784

785785
defmodule SystemLimitError do
786-
defexception []
787-
788-
@impl true
789-
def message(_) do
790-
"a system limit has been reached"
791-
end
786+
defexception message: "a system limit has been reached"
792787
end
793788

794789
defmodule SyntaxError do
@@ -1417,16 +1412,32 @@ defmodule ErlangError do
14171412
end
14181413

14191414
@doc false
1420-
def normalize(:badarg, _stacktrace) do
1421-
%ArgumentError{}
1415+
def normalize(:badarg, stacktrace) do
1416+
case error_info(:badarg, stacktrace) do
1417+
{:ok, args} ->
1418+
message = "errors were found at the given arguments:\n\n#{args}"
1419+
%ArgumentError{message: message}
1420+
1421+
:error ->
1422+
%ArgumentError{}
1423+
end
14221424
end
14231425

14241426
def normalize(:badarith, _stacktrace) do
14251427
%ArithmeticError{}
14261428
end
14271429

1428-
def normalize(:system_limit, _stacktrace) do
1429-
%SystemLimitError{}
1430+
def normalize(:system_limit, stacktrace) do
1431+
case error_info(:system_limit, stacktrace) do
1432+
{:ok, args} ->
1433+
message =
1434+
"a system limit has been reached due to errors at the given arguments:\n\n#{args}"
1435+
1436+
%SystemLimitError{message: message}
1437+
1438+
:error ->
1439+
%SystemLimitError{}
1440+
end
14301441
end
14311442

14321443
def normalize(:cond_clause, _stacktrace) do
@@ -1516,4 +1527,31 @@ defmodule ErlangError do
15161527
defp from_stacktrace(_) do
15171528
{nil, nil, nil}
15181529
end
1530+
1531+
@doc false
1532+
def error_info(erl_exception, stacktrace) do
1533+
with [{module, _, args_or_arity, opts} | _] <- stacktrace,
1534+
%{} = error_info <- opts[:error_info] do
1535+
module = Map.get(error_info, :module, module)
1536+
function = Map.get(error_info, :function, :format_error)
1537+
arity = if is_integer(args_or_arity), do: args_or_arity, else: length(args_or_arity)
1538+
extra = apply(module, function, [erl_exception, stacktrace])
1539+
args_errors = Map.take(extra, Enum.to_list(1..arity//1))
1540+
1541+
if map_size(args_errors) > 0 do
1542+
{:ok, IO.iodata_to_binary(Enum.map(args_errors, &arg_error/1))}
1543+
else
1544+
:error
1545+
end
1546+
else
1547+
_ -> :error
1548+
end
1549+
end
1550+
1551+
defp arg_error({n, message}), do: " * #{nth(n)} argument: #{message}\n"
1552+
1553+
defp nth(1), do: "1st"
1554+
defp nth(2), do: "2nd"
1555+
defp nth(3), do: "3rd"
1556+
defp nth(n), do: "#{n}th"
15191557
end

lib/elixir/lib/io.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,8 @@ defmodule IO do
108108
argument error. For example, let's try to put a code point that is not
109109
representable with one byte, like `?π`, inside IO data:
110110
111-
iex> IO.iodata_to_binary(["The symbol for pi is: ", ?π])
112-
** (ArgumentError) argument error
111+
IO.iodata_to_binary(["The symbol for pi is: ", ?π])
112+
#=> ** (ArgumentError) argument error
113113
114114
If we use chardata instead, it will work as expected:
115115

lib/elixir/lib/kernel.ex

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -535,12 +535,14 @@ defmodule Kernel do
535535
hd([1, 2, 3, 4])
536536
#=> 1
537537
538-
hd([])
539-
** (ArgumentError) argument error
540-
541538
hd([1 | 2])
542539
#=> 1
543540
541+
Giving it an empty list raises:
542+
543+
tl([])
544+
#=> ** (ArgumentError) argument error
545+
544546
"""
545547
@doc guard: true
546548
@spec hd(nonempty_maybe_improper_list(elem, any)) :: elem when elem: term
@@ -1186,9 +1188,6 @@ defmodule Kernel do
11861188
tl([1, 2, 3, :go])
11871189
#=> [2, 3, :go]
11881190
1189-
tl([])
1190-
** (ArgumentError) argument error
1191-
11921191
tl([:one])
11931192
#=> []
11941193
@@ -1198,6 +1197,11 @@ defmodule Kernel do
11981197
tl([:a | %{b: 1}])
11991198
#=> %{b: 1}
12001199
1200+
Giving it an empty list raises:
1201+
1202+
tl([])
1203+
#=> ** (ArgumentError) argument error
1204+
12011205
"""
12021206
@doc guard: true
12031207
@spec tl(nonempty_maybe_improper_list(elem, tail)) :: maybe_improper_list(elem, tail) | tail

lib/elixir/lib/list.ex

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -831,9 +831,6 @@ defmodule List do
831831
iex> List.to_existing_atom('🌢 Elixir')
832832
:"🌢 Elixir"
833833
834-
iex> List.to_existing_atom('this_atom_will_never_exist')
835-
** (ArgumentError) argument error
836-
837834
"""
838835
@spec to_existing_atom(charlist) :: atom
839836
def to_existing_atom(charlist) do

lib/elixir/lib/module.ex

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -724,9 +724,6 @@ defmodule Module do
724724
725725
## Examples
726726
727-
iex> Module.safe_concat([Module, Unknown])
728-
** (ArgumentError) argument error
729-
730727
iex> Module.safe_concat([List, Chars])
731728
List.Chars
732729
@@ -745,9 +742,6 @@ defmodule Module do
745742
746743
## Examples
747744
748-
iex> Module.safe_concat(Module, Unknown)
749-
** (ArgumentError) argument error
750-
751745
iex> Module.safe_concat(List, Chars)
752746
List.Chars
753747

lib/elixir/lib/string.ex

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2384,9 +2384,6 @@ defmodule String do
23842384
iex> String.to_existing_atom("my_atom")
23852385
:my_atom
23862386
2387-
iex> String.to_existing_atom("this_atom_will_never_exist")
2388-
** (ArgumentError) argument error
2389-
23902387
"""
23912388
@spec to_existing_atom(String.t()) :: atom
23922389
def to_existing_atom(string) when is_binary(string) do

lib/elixir/src/elixir_erl_try.erl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
-export([clauses/3]).
33
-include("elixir.hrl").
44
-define(REQUIRES_STACKTRACE,
5-
['Elixir.FunctionClauseError', 'Elixir.UndefinedFunctionError', 'Elixir.KeyError']).
5+
['Elixir.FunctionClauseError', 'Elixir.UndefinedFunctionError',
6+
'Elixir.KeyError', 'Elixir.ArgumentError', 'Elixir.SystemLimitError']).
67

78
clauses(_Meta, Args, S) ->
89
Catch = elixir_erl_clauses:get_clauses('catch', Args, 'catch'),

lib/elixir/test/elixir/exception_test.exs

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,9 @@ defmodule ExceptionTest do
231231

232232
return = {:ok, {{:foo, 1, 1}, []}}
233233
{:error, reason} = __MODULE__.Sup.start_link(fn -> return end)
234-
assert Exception.format_exit(reason) == "bad supervisor configuration, invalid strategy: :foo"
234+
235+
assert Exception.format_exit(reason) ==
236+
"bad supervisor configuration, invalid strategy: :foo"
235237

236238
return = {:ok, {{:one_for_one, :foo, 1}, []}}
237239
{:error, reason} = __MODULE__.Sup.start_link(fn -> return end)
@@ -261,7 +263,9 @@ defmodule ExceptionTest do
261263

262264
return = {:ok, {{:one_for_one, 1, 1}, [{:child, {:m, :f, []}, :foo, 1, :worker, []}]}}
263265
{:error, reason} = __MODULE__.Sup.start_link(fn -> return end)
264-
assert Exception.format_exit(reason) =~ "bad child specification, invalid restart type: :foo"
266+
267+
assert Exception.format_exit(reason) =~
268+
"bad child specification, invalid restart type: :foo"
265269

266270
return = {
267271
:ok,
@@ -275,7 +279,9 @@ defmodule ExceptionTest do
275279
{:error, reason} = __MODULE__.Sup.start_link(fn -> return end)
276280
assert Exception.format_exit(reason) =~ "bad child specification, invalid child type: :foo"
277281

278-
return = {:ok, {{:one_for_one, 1, 1}, [{:child, {:m, :f, []}, :temporary, 1, :worker, :foo}]}}
282+
return =
283+
{:ok, {{:one_for_one, 1, 1}, [{:child, {:m, :f, []}, :temporary, 1, :worker, :foo}]}}
284+
279285
{:error, reason} = __MODULE__.Sup.start_link(fn -> return end)
280286
assert Exception.format_exit(reason) =~ "bad child specification, invalid modules: :foo"
281287

@@ -705,8 +711,6 @@ defmodule ExceptionTest do
705711
end
706712
end
707713

708-
## Exception messages
709-
710714
describe "exception messages" do
711715
import Exception, only: [message: 1]
712716

@@ -768,4 +772,45 @@ defmodule ExceptionTest do
768772
assert %ErlangError{original: :sample} |> message == "Erlang error: :sample"
769773
end
770774
end
775+
776+
if :erlang.system_info(:otp_release) >= '24' do
777+
describe "error_info" do
778+
test "badarg on erlang" do
779+
assert message(:erlang, & &1.element("foo", "bar")) == """
780+
errors were found at the given arguments:
781+
782+
* 1st argument: not an integer
783+
* 2nd argument: not a tuple
784+
"""
785+
end
786+
787+
test "badarg on ets" do
788+
ets = :ets.new(:foo, [])
789+
:ets.delete(ets)
790+
791+
assert message(:ets, & &1.insert(ets, 1)) == """
792+
errors were found at the given arguments:
793+
794+
* 1st argument: the table identifier does not refer to an existing ETS table
795+
* 2nd argument: not a tuple
796+
"""
797+
end
798+
799+
test "system_limit on counters" do
800+
assert message(:counters, & &1.new(123_456_789_123_456_789_123_456_789, [])) == """
801+
a system limit has been reached due to errors at the given arguments:
802+
803+
* 1st argument: counters array size reached a system limit
804+
"""
805+
end
806+
807+
defp message(arg, fun) do
808+
try do
809+
fun.(arg)
810+
rescue
811+
e -> Exception.message(e)
812+
end
813+
end
814+
end
815+
end
771816
end

lib/elixir/test/elixir/kernel_test.exs

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1343,22 +1343,15 @@ defmodule KernelTest do
13431343
test "tl/1" do
13441344
assert tl([:one]) == []
13451345
assert tl([1, 2, 3]) == [2, 3]
1346-
1347-
assert_raise ArgumentError, "argument error", fn ->
1348-
tl(empty_list())
1349-
end
1346+
assert_raise ArgumentError, fn -> tl(empty_list()) end
13501347

13511348
assert tl([:a | :b]) == :b
13521349
assert tl([:a, :b | :c]) == [:b | :c]
13531350
end
13541351

13551352
test "hd/1" do
13561353
assert hd([1, 2, 3, 4]) == 1
1357-
1358-
assert_raise ArgumentError, "argument error", fn ->
1359-
hd(empty_list())
1360-
end
1361-
1354+
assert_raise ArgumentError, fn -> hd(empty_list()) end
13621355
assert hd([1 | 2]) == 1
13631356
end
13641357

0 commit comments

Comments
 (0)