Raise - dialyzer warning

defmodule MyAppError do
  defexception [:message]

  def exception(value) do
    %MyAppError{message: "Oops!: #{inspect value}"}
  end

  def test() do
    #raise MyAppError, "something wrong" # [1]
    raise MyAppError, %{a: 10, b: 20} # [2]
  end
end

Typespec for the exception callback is defined to accept term.

[1] Does not generate a warning
[2] Generates a warning

Not sure if this is a dialyzer issue.

The problem could be due to the fact that defexception generates a default typespec for exception/1 which takes a string.

Maybe overriding the spec might work (don’t have the time to try it out)?

defmodule MyAppError do
  # ...

  @spec exception(term) :: Exception.t
  def exception(value) do
    %MyAppError{message: "Oops!: #{inspect value}"}
  end

 # ...
end
1 Like

I think your point about what causes problem is correct. Howver, overriding the spec causes the has overlapping domains; such contracts are currently unsupported and are simply ignored message.

@spec with term as input parameter type causes the overlapping domains dialyzer error. Being specific with parameter type resolves this.

@spec exception(map) :: Exception.t
1 Like

Yeah, apparently, @spec doesn’t override the default spec injected by defexception. If I get this right, this means that even in your case (@spec exception(map) :: Exception.t), you add extra specification, so your exception can now take a string as well as a map.

I’m not sure if defexception should generate @spec. Let’s ping @josevalim to hear his thoughts on the matter.

In my opinion the pregenerated typespec is a bug, since the callback does allow a much broader type than String.t only.

Therefore it should be exclusively in the hands of the implementor to narrow the spec down from that. Injecting the spec premature even makes it impossible in terms of dialyzer to actually accept term… (without having to specify numbers, atoms, tuples, functions, etc separately)

1 Like

Agreed. The specs have been removed from master as otherwise we are unable to customize them.

4 Likes

What about enhancing the syntax with something that is currently invalid, something like:

defexception [:message] # Generates %__MODULE__{exception: true, message: nil} or so

# or a spec'd version:
defexception [:message :: String.t] # Generates %__MODULE__{exception: true, message: nil} or so with a spec that sets message to String.t

# Spec with a default
defexception [message: "default" :: String.t] # Etc...

# And more!
defexception [message: "default" :: String.t, blah: :undefined :: atom(), meta: nil] # meta defaults to `term()` since not specified

All syntax’s are valid and they are pretty trivially parseable to generate the same code as it does not but with an appropriately correct spec. :slight_smile:

1 Like

Here is another one related to raise/dialyzer:

defmodule ErrorMatching do
  @spec f1(boolean) :: {:ok, :yep_true} | {:error, :not_true}
  def f1(true), do: {:ok, :yep_true}
  def f1(false), do: {:error, :not_true}

  @spec f2(atom) :: {:ok, atom} | {:error, atom}
  def f2(:throwup), do: {:error, :throwup}
  def f2(b) when is_atom(b), do: {:ok, b}

  @spec is_it_ok?(boolean, atom) :: :ok | :error | no_return
  def is_it_ok?(a, b) do
    with {:f1, {:ok, _}} <- {:f1, f1(a)},
         {:f2, {:ok, _}} <- {:f2, f2(b)}
    do
      :ok
    else
      {:f1, {:error, _reason}} -> :error          # (i)
      {:f2, {:error, _reason}} -> raise "barf..." # (ii)
      _ -> :error
    end
  end
end

The above example generates the following dialyzer warning:

lib/error_matching.ex:17: The pattern {'f1', {'error', _reason@1}}
can never match the type {'f2',{'error','throwup'}}

The dialyzer warning goes away if lines (i) and (ii) are switched.