Skip to content

Return a cast error when Money returns an exception - #46

Closed
rubas wants to merge 1 commit into
ex-money:mainfrom
rubas:fix/cast-exception-errors
Closed

rubas wants to merge 1 commit into
ex-money:mainfrom
rubas:fix/cast-exception-errors

Conversation

@rubas

@rubas rubas commented Sep 26, 2026

Copy link
Copy Markdown

Before

iex> params = Money.Ecto.Composite.Type.init(locale: "!invalid!")
iex> Money.Ecto.Composite.Type.cast("100 CHF", params)
{:ok, {:error, %Localize.InvalidLocaleError{locale_id: "!invalid!"}}}
iex> Money.Ecto.Composite.Type.cast(%{"currency" => "CHF", "amount" => "100"}, params)
** (CaseClauseError) no case clause matching:
    {:error, %Localize.InvalidLocaleError{locale_id: "!invalid!"}}

Problem

  • A string cast succeeds with an error tuple as the value, so the changeset looks valid.
  • A map cast crashes.
  • Money.Ecto.Map.Type delegates cast/2, so it has the same bug.

After

defp cast_result(%Money{} = money), do: {:ok, money}

defp cast_result({:error, {exception, message}}),
  do: {:error, exception: exception, message: message}

defp cast_result({:error, exception}) when is_exception(exception),
  do: {:error, exception: exception.__struct__, message: Exception.message(exception)}
iex> Money.Ecto.Composite.Type.cast("100 CHF", params)
{:error, [exception: Localize.InvalidLocaleError, message: "The locale \"!invalid!\" is not valid."]}
iex> Money.Ecto.Composite.Type.cast(%{"currency" => "CHF", "amount" => "100"}, params)
{:error, [exception: Localize.InvalidLocaleError, message: "The locale \"!invalid!\" is not valid."]}
Details
  • Cause: Money.Ecto.Composite.Type.cast/2 matches only {:error, {module, message}}. Money.new/3 and Money.parse/2 return {:error, exception} for errors from Localize.
  • Both shapes now go through cast_result/1 and keep the existing exception: and message: keyword error.
  • The three map clauses for an integer, binary, and Decimal amount had the same body, so they are now one clause.
  • Tests: two cases in test/money_ecto_edge_case_test.exs, for both types and for string, string-keyed map, and atom-keyed map input.
  • mix test (179 passed, 5 skipped, PostgreSQL 17) and mix format --check-formatted pass.

AI disclosure: AI models helped us find this issue, write the change, and review it.

Money returns {:error, exception} for Localize errors such as an invalid
locale. Casting a string then returned {:ok, {:error, exception}}, and
casting a map raised CaseClauseError.

Convert both error shapes into Ecto cast errors in one place, and merge the
three identical map clauses.
@kipcole9

Copy link
Copy Markdown
Collaborator

Thanks for this, and for the clear write-up. The fix landed on main in the same shape as your cast_result/1, with the credit in the 2.2.0 changelog, and ships in ex_money_sql 2.2.0 on ex_money 6.3 today. Closing in favour of that release - which includes other changes to support ex_money 6.3.

@kipcole9 kipcole9 closed this Oct 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants