Skip to content

Commit 7dd9da1

Browse files
author
José Valim
committed
Promote exception/1 as the best mechanism to customize exceptions
The previous mechanism of lazily customizing message/1 was very dangerous, as you could have errors while calculating the message and those would shop up just when the exception is printed, leading to further breakage. Closes #1747
1 parent 67109b2 commit 7dd9da1

File tree

13 files changed

+155
-101
lines changed

13 files changed

+155
-101
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# v0.11.3-dev
22

33
* Enhancements
4+
* [Exception] Allow `exception/1` to be overriden and promote it as the main mechanism to customize exceptions
45
* [Kernel] Add `List.delete_at/2` and `List.updated_at/3`
56
* [Kernel] Add `Enum.reverse/2`
67
* [Kernel] Implement `defmodule/2`, `@/1`, `def/2` and friends in Elixir itself. `case/2`, `try/2` and `receive/1` have been made special forms. `var!/1`, `var!/2` and `alias!/1` have also been implemented in Elixir and demoted from special forms

lib/elixir/lib/code.ex

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
defmodule Code do
2-
defexception LoadError, file: nil do
3-
def message(exception) do
4-
"could not load #{exception.file}"
2+
defexception LoadError, [:file, :message] do
3+
def exception(opts) do
4+
file = opts[:file]
5+
LoadError[message: "could not load #{file}", file: file]
56
end
67
end
78

@@ -10,7 +11,6 @@ defmodule Code do
1011
1112
This module complements [Erlang's code module](http://www.erlang.org/doc/man/code.html)
1213
to add behavior which is specific to Elixir.
13-
1414
"""
1515

1616
@doc """

lib/elixir/lib/exception.ex

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# Some exceptions implement `message/1` instead of `exception/1` mostly
2+
# for bootstrap reasons. It is recommended for applications to implement
3+
# `exception/1` instead of `message/1` as described in `defexception/3` docs.
4+
15
defexception RuntimeError, message: "runtime error"
26
defexception ArgumentError, message: "argument error"
37
defexception ArithmeticError, message: "bad argument in arithmetic expression"
@@ -100,6 +104,22 @@ defexception Enum.OutOfBoundsError, message: "out of bounds error"
100104

101105
defexception Enum.EmptyError, message: "empty error"
102106

107+
defexception File.Error, [reason: nil, action: "", path: nil] do
108+
def message(exception) do
109+
formatted = iolist_to_binary(:file.format_error(reason exception))
110+
"could not #{action exception} #{path exception}: #{formatted}"
111+
end
112+
end
113+
114+
defexception File.CopyError, [reason: nil, action: "", source: nil, destination: nil, on: nil] do
115+
def message(exception) do
116+
formatted = iolist_to_binary(:file.format_error(reason exception))
117+
location = if on = on(exception), do: ". #{on}", else: ""
118+
"could not #{action exception} from #{source exception} to " <>
119+
"#{destination exception}#{location}: #{formatted}"
120+
end
121+
end
122+
103123
defmodule Exception do
104124
@moduledoc """
105125
Convenience functions to work with and pretty print

lib/elixir/lib/file.ex

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -31,22 +31,6 @@ defrecord File.Stat, Record.extract(:file_info, from_lib: "kernel/include/file.h
3131
"""
3232
end
3333

34-
defexception File.Error, [reason: nil, action: "", path: nil] do
35-
def message(exception) do
36-
formatted = iolist_to_binary(:file.format_error(reason exception))
37-
"could not #{action exception} #{path exception}: #{formatted}"
38-
end
39-
end
40-
41-
defexception File.CopyError, [reason: nil, action: "", source: nil, destination: nil, on: nil] do
42-
def message(exception) do
43-
formatted = iolist_to_binary(:file.format_error(reason exception))
44-
location = if on = on(exception), do: ". #{on}", else: ""
45-
"could not #{action exception} from #{source exception} to " <>
46-
"#{destination exception}#{location}: #{formatted}"
47-
end
48-
end
49-
5034
defmodule File do
5135
@moduledoc """
5236
This module contains functions to manipulate files.

lib/elixir/lib/io.ex

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
defexception IO.StreamError, reason: nil do
2-
def message(exception) do
3-
formatted = iolist_to_binary(:file.format_error(reason exception))
4-
"error during streaming: #{formatted}"
1+
defexception IO.StreamError, [:reason, :message] do
2+
def exception(opts) do
3+
reason = opts[:reason]
4+
formatted = iolist_to_binary(:file.format_error(reason))
5+
IO.StreamError[message: "error during streaming: #{formatted}", reason: reason]
56
end
67
end
78

lib/elixir/lib/kernel.ex

Lines changed: 70 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1466,15 +1466,34 @@ defmodule Kernel do
14661466
14671467
"""
14681468
@spec raise(binary | atom | tuple) :: no_return
1469-
defmacro raise(msg) when is_binary(msg) do
1470-
quote do
1471-
:erlang.error RuntimeError.exception(message: unquote(msg))
1469+
defmacro raise(msg) do
1470+
# Try to figure out the type at compilation time
1471+
# to avoid dead code and make dialyzer happy.
1472+
msg = case not is_binary(msg) and bootstraped?(Macro.Env) do
1473+
true -> Macro.expand(msg, __CALLER__)
1474+
false -> msg
14721475
end
1473-
end
14741476

1475-
defmacro raise({ tag, _, _ } = exception) when tag == :<<>> or tag == :<> do
1476-
quote do
1477-
:erlang.error RuntimeError.exception(message: unquote(exception))
1477+
case msg do
1478+
msg when is_binary(msg) ->
1479+
quote do
1480+
:erlang.error RuntimeError.exception(message: unquote(msg))
1481+
end
1482+
{ :<<>>, _, _ } = msg ->
1483+
quote do
1484+
:erlang.error RuntimeError.exception(message: unquote(msg))
1485+
end
1486+
alias when is_atom(alias) ->
1487+
quote do
1488+
:erlang.error unquote(alias).exception([])
1489+
end
1490+
_ ->
1491+
quote do
1492+
case unquote(msg) do
1493+
msg when is_binary(msg) -> :erlang.error RuntimeError.exception(message: msg)
1494+
msg -> :erlang.error msg.exception([])
1495+
end
1496+
end
14781497
end
14791498
end
14801499

@@ -1486,8 +1505,7 @@ defmodule Kernel do
14861505
structure.
14871506
14881507
Any module defined via `defexception` automatically
1489-
defines both `exception(args)` and `exception(args, current)`
1490-
that creates a new and updates the given exception.
1508+
implements `exception(args)` callback expected by `raise/2`.
14911509
14921510
## Examples
14931511
@@ -1496,35 +1514,9 @@ defmodule Kernel do
14961514
14971515
"""
14981516
@spec raise(tuple | atom, list) :: no_return
1499-
defmacro raise(exception, args // [])
1500-
1501-
defmacro raise({ :{}, _, _ } = exception, args) do
1502-
quote do
1503-
:erlang.error unquote(exception).exception(unquote(args))
1504-
end
1505-
end
1506-
1507-
defmacro raise({ :__aliases__, _, _ } = exception, args) do
1508-
quote do
1509-
:erlang.error unquote(exception).exception(unquote(args))
1510-
end
1511-
end
1512-
1513-
defmacro raise(exception, args) when is_atom(exception) do
1514-
quote do
1515-
:erlang.error unquote(exception).exception(unquote(args))
1516-
end
1517-
end
1518-
15191517
defmacro raise(exception, args) do
15201518
quote do
1521-
exception = unquote(exception)
1522-
case exception do
1523-
e when is_binary(e) ->
1524-
:erlang.error RuntimeError.exception(message: exception)
1525-
_ ->
1526-
:erlang.error exception.exception(unquote(args))
1527-
end
1519+
:erlang.error unquote(exception).exception(unquote(args))
15281520
end
15291521
end
15301522

@@ -1551,8 +1543,10 @@ defmodule Kernel do
15511543
may change the `System.stacktrace` value.
15521544
"""
15531545
@spec raise(tuple | atom, list, list) :: no_return
1554-
def raise(exception, args, stacktrace) do
1555-
:erlang.raise :error, exception.exception(args), stacktrace
1546+
defmacro raise(exception, args, stacktrace) do
1547+
quote do
1548+
:erlang.raise :error, unquote(exception).exception(unquote(args)), unquote(stacktrace)
1549+
end
15561550
end
15571551

15581552
@doc """
@@ -3182,18 +3176,47 @@ defmodule Kernel do
31823176
Record.defrecordp(name, Macro.expand(tag, __CALLER__), fields)
31833177
end
31843178

3185-
@doc """
3179+
@doc %S"""
31863180
Defines an exception.
31873181
3188-
Exceptions are simply records and therefore `defexception/3` has
3189-
the same API and similar behavior to `defrecord/3` with two notable
3190-
differences:
3182+
Exceptions are simply records with three differences:
3183+
3184+
1. Exceptions are required to defined a function `exception/1`
3185+
that receives keyword arguments and returns the exception.
3186+
This function is a callback usually invoked by `raise/2`;
3187+
3188+
2. Exceptions are required to provide a `message` field.
3189+
This field must return a String with a formatted error message;
3190+
3191+
3. Unlike records, exceptions are documented by default.
3192+
3193+
Since exceptions are records, `defexception/3` has exactly
3194+
the same API as `defrecord/3`.
3195+
3196+
## Raising exceptions
31913197
3192-
1) Unlike records, exceptions are documented by default;
3198+
The most common way to raise an exception is via the raise
3199+
function:
3200+
3201+
defexception MyException, [:message]
3202+
raise MyException,
3203+
message: "did not get what expected, got: #{inspect value}"
3204+
3205+
In many cases though, it is more convenient to just pass the
3206+
expected value to raise and generate the message in the `exception/1`
3207+
callback:
3208+
3209+
defexception MyException, [:message] do
3210+
def exception(opts) do
3211+
msg = "did not get what expected, got: #{inspect opts[:actual]}"
3212+
MyException[message: msg]
3213+
end
3214+
end
31933215
3194-
2) Exceptions **must** implement `message/1` -- a function that returns a
3195-
string;
3216+
raise MyException, actual: value
31963217
3218+
The example above is the preferred mechanism for customizing
3219+
exception messages.
31973220
"""
31983221
defmacro defexception(name, fields, do_block // []) do
31993222
{ fields, do_block } =
@@ -3204,14 +3227,15 @@ defmodule Kernel do
32043227

32053228
do_block = Keyword.put(do_block, :do, quote do
32063229
@moduledoc nil
3207-
record_type message: binary
3230+
record_type message: String.t
32083231

32093232
@doc false
32103233
def exception(args), do: new(args)
32113234

32123235
@doc false
32133236
def exception(args, self), do: update(args, self)
32143237

3238+
defoverridable exception: 1, exception: 2
32153239
unquote(Keyword.get do_block, :do)
32163240
end)
32173241

lib/elixir/lib/module.ex

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -677,7 +677,12 @@ defmodule Module do
677677
raise "Cannot make function #{name}/#{arity} overridable because it was not defined"
678678
clause ->
679679
:elixir_def.delete_definition(module, tuple)
680-
neighbours = Module.DispatchTracker.yank(module, tuple)
680+
681+
neighbours = if loaded?(Module.DispatchTracker) do
682+
Module.DispatchTracker.yank(module, tuple)
683+
else
684+
[]
685+
end
681686
682687
old = get_attribute(module, :__overridable)
683688
merged = :orddict.update(tuple, fn({ count, _, _, _ }) ->
@@ -942,4 +947,6 @@ defmodule Module do
942947
raise ArgumentError,
943948
message: "could not call #{fun} on module #{inspect module} because it was already compiled"
944949
end
950+
951+
defp loaded?(module), do: is_tuple :code.is_loaded(module)
945952
end

lib/elixir/lib/string.ex

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1056,9 +1056,12 @@ defmodule String do
10561056
raise ArgumentError
10571057
end
10581058

1059-
defexception UnicodeConversionError, encoded: nil, rest: nil, kind: nil do
1060-
def message(exception) do
1061-
"#{exception.kind} #{detail(exception.rest)}"
1059+
defexception UnicodeConversionError, [:encoded, :message] do
1060+
def exception(opts) do
1061+
UnicodeConversionError[
1062+
encoded: opts[:encoded],
1063+
message: "#{opts[:kind]} #{detail(opts[:rest])}"
1064+
]
10621065
end
10631066

10641067
defp detail(rest) when is_binary(rest) do

lib/elixir/lib/version.ex

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ defmodule Version do
22
@moduledoc %S"""
33
Functions for parsing and matching versions against requirements.
44
5-
A version is a string in a specific format or a `Version.Schema`
5+
A version is a string in a specific format or a `Version.Schema`
66
generated after parsing via `Version.parse/1`.
77
88
`Version` parsing and requirements follow
@@ -31,7 +31,7 @@ defmodule Version do
3131
3232
Requirements allow you to specify which versions of a given
3333
dependency you are willing to work against. It supports common
34-
operators like `>=`, `<=`, `>`, `==` and friends that
34+
operators like `>=`, `<=`, `>`, `==` and friends that
3535
work as one would expect:
3636
3737
# Only version 2.0.0
@@ -65,22 +65,25 @@ defmodule Version do
6565
defrecord Schema, major: 0, minor: 0, patch: 0, pre: nil, build: nil, source: nil
6666
defrecord Requirement, source: nil, matchspec: nil
6767

68-
defexception InvalidRequirement, reason: :invalid_requirement do
69-
def message(InvalidRequirement[reason: reason]) when is_binary(reason) do
70-
{ first, rest } = String.next_grapheme(reason)
71-
String.downcase(first) <> rest
72-
end
68+
defexception InvalidRequirement, [:message] do
69+
def exception(opts) do
70+
message =
71+
if is_binary opts[:reason] do
72+
{ first, rest } = String.next_grapheme(opts[:reason])
73+
String.downcase(first) <> rest
74+
else
75+
"invalid version specification"
76+
end
7377

74-
def message(InvalidRequirement[]) do
75-
"invalid version specification"
78+
InvalidRequirement[message: message]
7679
end
7780
end
7881

7982
@doc """
8083
Check if the given version matches the specification.
8184
8285
Returns `true` if `version` satisfies `requirement`, `false` otherwise.
83-
Raises a `Version.InvalidRequirement` exception if `requirement` is not parseable.
86+
Raises a `Version.InvalidRequirement` exception if `requirement` is not parseable.
8487
8588
## Examples
8689

lib/elixir/src/elixir_def_overridable.erl

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,12 @@ store(Module, Function, GenerateName) ->
4343
false -> { Kind, Name }
4444
end,
4545

46-
'Elixir.Module.DispatchTracker':reattach(Module, Kind, { Name, Arity }, Neighbours),
46+
case code:is_loaded('Elixir.Module.DispatchTracker') of
47+
{ _, _ } ->
48+
'Elixir.Module.DispatchTracker':reattach(Module, Kind, { Name, Arity }, Neighbours);
49+
_ ->
50+
ok
51+
end,
4752

4853
Def = { function, Line, FinalName, Arity, Clauses },
4954
elixir_def:store_each(false, FinalKind, File, Location,

0 commit comments

Comments
 (0)