Skip to content

Keyword-list branch of to_error/2 crashes unless the unknown error declares a :value field #126

Description

@jimsynz

Summary

to_error/2's keyword-list branch passes a :value option to the configured unknown_error unconditionally. Only Ash.Error.Unknown.UnknownError happens to declare that field (fields: [:error, :field, :value]), so for any other unknown error struct!/2 raises KeyError: key :value not found.

This makes the branch unusable outside Ash, including for the fields: [:error] shape used throughout splode's own documentation and tests.

Reproduction

Using the same shape as splode's own test suite:

defmodule Own.UnknownError do
  use Splode.Error, fields: [:error], class: :unknown
  def message(e), do: "unknown: #{inspect(e.error)}"
end

defmodule Own.Unknown do
  use Splode.ErrorClass, class: :unknown
end

defmodule Own.Errors do
  use Splode, error_classes: [unknown: Own.Unknown], unknown_error: Own.UnknownError
end

Every keyword list crashes, including the intended one:

[noproc: {GenServer, :call, [:n, :ping, 5000]}]  => {:raised, KeyError, "key :value not found"}
[timeout: 5000]                                  => {:raised, KeyError, "key :value not found"}
[shutdown: :closed]                              => {:raised, KeyError, "key :value not found"}
[message: "a real error opts list"]              => {:raised, KeyError, "key :value not found"}

The README documents returning "a string, or a keyword list" as supported, so this is the documented path.

Second, related problem: the branch is entered too eagerly

The branch is selected with Keyword.keyword?/1, which is true for any list of two-element tuples with atom keys. OTP exit reasons qualify:

Keyword.keyword?([{:noproc, {GenServer, :call, [:some_name, :ping, 5000]}}])
#=> true

So an exit reason gets destructured as a set of options — error: nil (there is no :message key), with the reason itself put in :value. Even with a :value field declared, the result renders as:

# Unknown Error
An unknown error occurred.
## `error`:
`nil`

The real reason is hidden in a field that most message/1 implementations don't render. Note that the same reason as a three-element tuple fails Keyword.keyword?/1, takes the catch-all, and renders correctly as unknown error: {:noproc, {GenServer, :call, [...]}} — so the keyword branch is strictly worse than the fallback it pre-empts, and conversion depends on tuple arity.

This surfaced as a reactor bug report where an async step's task exit replaced the real error with KeyError: key :value not found. Filed for reactor separately, but the crash is reachable from any consumer.

Notes

  • Not an Elixir 1.20 change: defexception's generated exception/1 has been a bare struct!(__MODULE__, args) at least back through 1.17, so there is no unknown-field filtering to have lost.
  • Keyword.put_new(:value, list) has been present since the initial commit.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions