Slide 1

Slide 1 text

2021 Fumbling with Exceptions by Greg Vaughn

Slide 2

Slide 2 text

About me • @gvaughn or @gregvaughn on GitHub, Twitter, Slack, ElixirForum, etc. • Started learning Elixir in 2013 • Professionally since 2017 We're hiring!

Slide 3

Slide 3 text

"Let it Crash" or maybe not (yet) •Supervision trees are a wonderful safety net •Sequential vs. Concurrent code •Erlang uses errors/exits via throw/catch •Elixir brings exceptions to the party •Elixir wraps most Erlang errors with exceptions

Slide 4

Slide 4 text

Exception is a Behaviour defmodule Exception do @type t :: %{ required(:__struct__) => module, required(:__exception__) => true, optional(atom) => any } @callback exception(term) :: t @callback message(t) :: String.t() @callback blame(t, stacktrace) :: {t, stacktrace} @optional_callbacks [blame: 2] # 1500 lines later end

Slide 5

Slide 5 text

Exceptions are Structs # in Kernel defmacro defexception(fields) do quote bind_quoted: [fields: fields] do @behaviour Exception struct = defstruct([__exception__: true] ++ fields) # conditionally supply overridable message/1 if has a :message field @impl true def exception(args) when Kernel.is_list(args) do # some backwards compatibility concerns today, # but for Elixir 2.0 it will be simply Kernel.struct!(struct, args) end end defoverridable exception: 1 end

Slide 6

Slide 6 text

wait Exception is a Behaviour and Exceptions are Structs ? (make up your mind, Greg)

Slide 7

Slide 7 text

iex> defmodule Boo, do: defexception [:message, :extra] {:module, Boo, ... I Explore in iex base exception iex> inspect %Boo{}, structs: false "%{__exception__: true, __struct__: Boo, extra: nil, message: nil}" iex> Boo.module_info :attributes [vsn: [4258765674426512055423525333953], behaviour: [Exception]] iex> inspect %Boo{} "%Boo{extra: nil, message: nil}"

Slide 8

Slide 8 text

I Explore in iex raise iex> raiser = fn -> try do raise %Boo{message: "boo!"} ...> rescue (e -> IO.puts "caught #{inspect e}") ...> end end iex> raiser.() caught %Boo{extra: nil, message: "boo!"} iex> raiser2 = fn -> try do raise(Boo, message: "boo2!", extra: "scary") ...> rescue (e -> IO.puts "caught #{inspect e}") ...> end end iex> raiser2.() caught %Boo{extra: "scary", message: "boo2!"} iex> raiser3 = fn -> try do raise Boo.exception(message: "boo3!", extra: "scary") ...> rescue (e -> IO.puts "caught #{inspect e}") ...> end end iex> raiser3.() caught %Boo{extra: "scary", message: "boo3!"}

Slide 9

Slide 9 text

Three ways to raise • raise %Boo{message: "boo!"} • basic passthrough because it's already an Exception • raise(Boo, message: "boo2!", extra: "scary") • arity 2 that calls `Boo.exception/1` • raise Boo.exception(message: "boo3!", extra: "scary") • created by `defexception/1` • end up at `:erlang.error/1` • Raises an exception of class error with the arg as the reason, where reason is any term. • Generates the stacktrace

Slide 10

Slide 10 text

iex> defmodule Boo do ...> defexception [:message, :extra] ...> def blame(me, stack) do ...> {%{me | message: "the devil made Boo do it"}, stack} ...> end ...> end {:module, Boo, ... I Explore in iex de fl ecting the blame iex> raise Boo ** (Boo) the devil made Boo do it

Slide 11

Slide 11 text

Friendly blame defmodule KeyError do # snip def blame(%{term: term, key: key} = exception, stacktrace) do message = message(key, term) if is_atom(key) and (map_with_atom_keys_only?(term) or Keyword.keyword?(term)) do hint = did_you_mean(key, available_keys(term)) {%{exception | message: message <> IO.iodata_to_binary(hint)}, stacktrace} else {%{exception | message: message}, stacktrace} end end defp did_you_mean(missing_key, available_keys) do suggestions = for key <- available_keys, distance = String.jaro_distance(Atom.to_string(missing_key), Atom.to_string(key)), distance >= @threshold, do: {distance, key} case suggestions do [] -> [] suggestions -> [". Did you mean one of:\n\n" | format_suggestions(suggestions)] end end # snip end

Slide 12

Slide 12 text

Erlang errors Three classes • throws • expect other code to handle • longjump or goto • exit • a process' "dying breath" • links/monitors react to "death" • error • something bad happened, but maybe somebody (somecode) can handle it • includes stacktrace

Slide 13

Slide 13 text

Error handling Erlang/Elixir di ff erences • Erlang • try/catch/after • `catch` can pattern match errors/exits/throws • Elixir • try/rescue/catch/else/after • Erlang errors are wrapped (or ErlangError) in pattern match of `rescue` • `else` pattern matches the return value of the `try` body

Slide 14

Slide 14 text

Libraries vs. Applications vs. Frameworks Design concerns • Libraries • Utility code to be called • Don't make assumptions about what is exceptional (bang and non-bang options) • Applications • Caller of utility libraries • Decides what is worth catching or crashing about • Frameworks • Hollywood Principle: don't call us, we'll call you • Opinionated on what is worth catching or crashing about (with hooks, perhaps) • Your business domain logic is provided as callbacks/hooks to the framework

Slide 15

Slide 15 text

Exceptional Libraries O ff ering options • Use {:ok, value} or {:error, exception} • Bang versions are straightforward to write - case raise the exception • Unbang functions o ff er great fl exibility to callers • Simple case of with/else • {:error, exception} -> Logger.error(Exception.message(exception)) • Consider including `:reason` fi eld so pattern matches can be speci fi c

Slide 16

Slide 16 text

Meet the OrderFumble exception refactoring technique defmodule OrderFumble do @enforce_keys [:reason, :step, :context] defexception @enforce_keys def new(kw) do %OrderContext{} = Keyword.get(kw, :context) struct!(__MODULE__, kw) end @impl Exception def exception(kw), do: new(kw) @impl Exception def message(%__MODULE__{} = e) do "#{inspect(e.reason)} (in #{e.step})" end # snip end

Slide 17

Slide 17 text

Meet the OrderFumble exception refactoring technique defmodule OrderFumble do # snip @manual [inventory: :mismatched_counts] @retryable [payment: :timeout] def mitigate(%__MODULE__{} = e) do case {e.step, e.reason} do conditions when conditions in @manual -> handle_notification(e) conditions when conditions in @retryable -> handle_retry(e) _ -> raise e end {:error, e} end # snip handle_notification/1, handle_retry/1 end

Slide 18

Slide 18 text

Takeaways from an exceptional talk • Exceptions are both behaviours and structs (aka objects) • blame/2 with impunity for great developer experience • Consider returning {:error, exception} • Exception modules can be refactoring targets Thanks! • Catch me online as @gvaughn or @gregvaughn • We're hiring at • Questions?