Sometimes you may find yourself in the need to capture a Postgres
(or any other RDBMS) custom error in the Ecto.Changeset
without raising an exception. This enables you to handle all the errors in one place without braking your aesthetic Elixir functional code with “try/rescue” constructs.
It has one big advantage: as the Ecto.Changeset
is a “lingua franca” of many libraries and frameworks (like Phoenix), embedding error reports in the changeset struct will work for you out of the box without any additional error handing burden. Less code, less maintenance!
Under the hood
Currently, the Postgres Ecto Adapter
(same as other adapters for major RDBMS) provide only limited support for reporting errors inside the Ecto Changeset
. Let’s have a glimpse into the Postgres adapter source code:
@impl true
def to_constraints(%Postgrex.Error{postgres: %{code: :unique_violation, constraint: constraint}}, _opts),
do: [unique: constraint]
def to_constraints(%Postgrex.Error{postgres: %{code: :foreign_key_violation, constraint: constraint}}, _opts),
do: [foreign_key: constraint]
def to_constraints(%Postgrex.Error{postgres: %{code: :exclusion_violation, constraint: constraint}}, _opts),
do: [exclusion: constraint]
def to_constraints(%Postgrex.Error{postgres: %{code: :check_violation, constraint: constraint}}, _opts),
do: [check: constraint]
We can see that certain Postgres errors, namely those related to constraints, get a special treatment at the adapter level so that later could be transformed into relevant changeset errors on demand (by calling *_constraint
functions in the changeset). Meanwhile, the remaining errors will be let through and propagated to your code. There are only few constraint error codes that get intercepted:
- :unique_violation
- :foreign_key_violation
- :exclusion_violation
- :check_violation
Solution
The method I would like to propose is to disguise your custom database error as one of the constraints that is already implemented by default in the Postgres Ecto adapter (see above).
In this example, I will define and raise a custom error from within a PL/pgSQL trigger function using Postgres’ check_contraint
ERRCODE, but you can use any of the four, whichever makes more sense to you.
Step 1. Raise error in Postgres codebase.
CREATE FUNCTION custom_check() RETURNS TRIGGER AS $$
BEGIN
IF <SOME CONDITION> THEN
RAISE EXCEPTION 'CUSTOM ERROR'
USING ERRCODE = 'check_violation',
CONSTRAINT = 'name_of_your_contraint';
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql
where:
-
CUSTOM ERROR
is a custom string lateral of your choice that will be passed to Ecto as the error message text. -
ERRCODE
must be one of the following:unique_violation
foreign_key_violation
exclusion_violation
check_violation
-
CONSTRAINT
must have a name of your choice that will uniquely identify the custom error in the Ecto Changeset.
Please note: a comprehensive list of Postgres error codes can be found in the Postgres Documentation — Errors and Messages.
Step 2 Define standard constraint in Ecto Changeset.
In this case, I consistently follow check_contraint error code raised in Postgres and call check_constraint function in the changeset to capture it.
def changeset(schema, attrs) do
schema
|> check_constraint(:some_field, name: name_of_your_contraint: , message: "custom error message")
end
where:
-
:some_field
is a key of associated with the model struct. It is particularly useful when working with Phoenix forms. -
:name_of_your_contraint
is an atom reflecting the same name as the one defined in the Postgres codebase. -
message
is an error message on Ecto side that will from additional contextual information.
To make your Elixir code more readable, you could consider some refactoring:
def changeset(schema, attrs) do
schema
|> my_custom_error(:some_field)
end
defp my_custom_error(schema, key) do
schema
|> check_constraint(key, name: name_of_your_contraint: , message: "custom error message")
end
A minor trade off is that a potential error description in the changeset has to be related to a key in the existing Schema struct. This is because changesets are designed on field level. If you use Phoenix form, you can compensate this drawback with an accurate error message propagated to the user.
Summary
In this article, I tried to propose a fairly easy technique to intercept custom database errors and turn them into a Ecto Changeset
errors. This all without the need to override your Repo
module functionality nor forking the adapter’s code, which would be way more difficult to maintain with new Ecto
library updates.
Please, feel free to leave your comment and share other approaches that you came across.
Top comments (0)