Error struct in typespec

Do error struct modules have their types specified somewhere?

I’m trying to use something like:
@spec my_fun(FunctionClauseError.t()) :: :ok
(I need to pass the actual error struct).

I could use:
@spec my_fun(%FunctionClauseError{}) :: :ok
but this makes Dialyzer ignore struct field types and additionally it creates a compile-time dependency.

elixir/lib/elixir/lib/exception.ex at a64d42f5d3cb6c32752af9d3312897e8cd5bb7ec · elixir-lang/elixir · GitHub ?

1 Like

Plain defstructs don’t have field types. You could make an Exception-like struct using typed_struct, but since the generated exception/1 uses Kernel.struct!/2 it could still produce nonsense without being detected by Dialyzer. For instance, I wouldn’t expect the usage code below to complain about types despite being clearly wrong:

defmodule FancyTypedException do
  use TypedStruct

  typedstruct do
    field :__exception__, boolean(), default: true
    field :integer_only, non_neg_integer()
    field :message, String.t()
  end

  @behaviour Exception

  @impl Exception
  def message(exception) do
    exception.message
  end

  @impl Exception
  def exception(msg) when is_binary(msg) do
    exception(message: msg)
  end

  def exception(args) when is_list(args) do
    struct!(struct, args)
  end
end

usage code:

raise FancyTypedException, message: "wat", integer_only: "nope"

This will result in a FancyTypedException struct with a binary in integer_only. :scream_cat:

Writing exception/1 with a struct literal might help, but IIRC the challenge is that exception/1 is called via Kernel.apply at runtime so Dialyzer doesn’t have much visibility.

1 Like

Thanks, guys, but now I see that I may not have expressed clearly what I want to do.

I need to pass error structs that are in the standard library (so I don’t have control over them), e.g. FunctionClauseError: elixir/exception.ex at a64d42f5d3cb6c32752af9d3312897e8cd5bb7ec · elixir-lang/elixir · GitHub

The reason I need those types is that I’ve got functions that pass around those errors, and Credo will protest (since I’ve got Credo.Check.Warning.SpecWithStruct rule enabled).

@al2o3cr by “this makes Dialyzer ignore struct field types” I meant that in case of using %MyModule{} in typespec Dialyzer will only be able to check whether the arg is a map with field __struct__ = MyModule, and that’s all. Using something like MyModule.t() it would check struct field types as well, provided there is a type definition for it, but standard library error modules don’t have those types specified apparently.

Btw long time no see Josh @polygonpusher :slight_smile:

IMO there are two separate situations here, with distinct solutions:

  • in the generic, user-defined case: define MyModule.t() and use it. The Credo check may be useful for reducing compilation times

  • in the stdlib case: there isn’t any type information beyond “this is a map with __struct__: FunctionClauseError”. The Credo check doesn’t make any sense, as stdlib is compiled already! Use %FunctionClauseError{} and silence the check.

1 Like

You’re right, in this particular case, I should ignore the warning since the error module won’t be recompiled anyway. I guess I fixated on doing this “the right way”, since there may be situations in which I would build the error struct by hand to pass it, and I would like Dialyzer to be able to check the field types as well.
Thanks for your help!