Prettify Ecto errors

July 5, 2017
Hot topics 🔥
Tech Insights
David Roman
Prettify Ecto errors

By Sergey Gernyak, Back-End Engineer.


I came into Elixir from the Ruby and Rails world.

RoR has many cool libraries out of box, and, well, actually we are spoiled by the proposed functionality of them. Ecto was created with another philosophy to the ActiveRecord, but the ideas around the flow are similar.

For example, validation and getting error messages. In RoR using the ActiveRecord library you do the following:

post = Post.create title: '', body: ''
post.valid? # => false
post.errors.full_messages # => ["Title should not be blank!"]

With Ecto, this kind of code can look like the following:

{:error, changeset} = %Post{}
                      |> Post.changeset(%{title: "", body: ""})
                      |> Repo.insert
changeset.valid? # => false
changeset.errors # => [title: {"should not be blank", []}]

But… there is no built-in approach to get a complete list of error messages. There is only the traverse_errors/2 function where you can find an example of how to go through each error and process it.

During the development of one of my Elixir applications, I wrote the code below as preparation to prettify errors:

defmodule EctoHelper do
  @moduledoc """
  Provides helper functions

  @doc """
  Prettifies changeset error messages.
  By default `changeset.errors` returns errors as keyword list, where key is name of the field
  and value is part of message. For example, `[body: "is required"]`.
  This method transforms errors in list which is ready to pass it, for example, in response of
  a JSON API request.
  ## Example of basic usage
  EctoHelper.pretty_errors([body: "is required"]) # => ["Body is required"]
  ## Example of usage with interpolations
  EctoHelper.pretty_errors([login: {"should be at most %{count} character(s)", [count: 10]}])
  # => ["Login should be at most 10 character(s)"]
  @spec pretty_errors(Map.t) :: [String.t]
  def pretty_errors(errors) do

  defp do_prettify({field_name, message}) when is_bitstring(message) do
    human_field_name = field_name
                        |> Atom.to_string
                        |> String.replace("_", " ")
                        |> String.capitalize
    human_field_name <> " " <> message 
  defp do_prettify({field_name, {message, variables}}) do
    compound_message = do_interpolate(message, variables)
    do_prettify({field_name, compound_message})

  defp do_interpolate(string, [{name, value} | rest]) do
    n = Atom.to_string(name)
    msg = String.replace(string, "%{#{n}}", do_to_string(value))
    do_interpolate(msg, rest)
  defp do_interpolate(string, []), do: string

  defp do_to_string(value) when is_integer(value), do: Integer.to_string(value)
  defp do_to_string(value) when is_bitstring(value), do: value
  defp do_to_string(value) when is_atom(value), do: Atom.to_string(value)
defmodule EctoHelperTest do
  use ExUnit.Case, async: true

  test "prettify simple errors" do
    res = EctoHelper.pretty_errors([body: "is required"])
    assert ["Body is required"] = res

  test "prettify errors with variables" do
    res = EctoHelper.pretty_errors([
      login: {"should be at most %{count} character(s)", [count: 10]},
      message_body: "should not be blank"
    assert String.equivalent?(, 0), "Login should be at most 10 character(s)")
    assert String.equivalent?(, 1), "Message body should not be blank")

I hope this helps.

Enjoy! Happy hacking!

David Roman

David is one of our marketing gurus. He loves working with content but has a good eye for marketing analytics as well. Creativity is what drives him, photography being one of his passions.

Working Machines

An executive’s guide to AI and Intelligent Automation. Working Machines takes a look at how the renewed vigour for the development of Artificial Intelligence and Intelligent Automation technology has begun to change how businesses operate.