How do I read Phoenix errors?

I’m playing a game where I intentionally create errors in my programs to see if I can fix them based on console output alone.

Cliff notes answer: I can’t

In JS I feel like errors are more direct and I can fix a program by finding the file and line number, and working backwards.

In Elixir I wouldn’t be able to fix a codebase that is handed to me from another developer without the help of forums and Google.

So for example, this error below is the result of me changing a function argument.

I went from this:

  def handle_event("send", %{"text" => text}, socket) do     # This is what I changed below
    IO.inspect text
    Phoenix.PubSub.broadcast(App.PubSub, topic, {:text_stuff, text}) 
    {:noreply, socket}
  end

To this

  def handle_event("send", text, socket) do     # This is what I changed 
    IO.inspect text
    Phoenix.PubSub.broadcast(App.PubSub, topic, {:text_stuff, text}) 
    {:noreply, socket}
  end

The code is part of a larger Live View (below)

I would like to know how to use the errors to work backwards fix this. If anyone wants to share their thought process I would like to read it.

Error

`[error] GenServer #PID<0.1420.0> terminating
** (Protocol.UndefinedError) protocol Phoenix.HTML.Safe not implemented for %{"text" => "ad"} of type Map. This protocol is implemented for the following type(s): Atom, BitString, Date, DateTime, Decimal, Float, Integer, List, NaiveDateTime, Phoenix.HTML.Form, Phoenix.LiveComponent.CID, Phoenix.LiveView.Component, Phoenix.LiveView.Comprehension, Phoenix.LiveView.JS, Phoenix.LiveView.Rendered, Time, Tuple, URI
    (phoenix_html 3.3.2) lib/phoenix_html/safe.ex:1: Phoenix.HTML.Safe.impl_for!/1
    (phoenix_html 3.3.2) lib/phoenix_html/safe.ex:15: Phoenix.HTML.Safe.to_iodata/1
    (app 0.1.0) lib/app_web/live/send_live.ex:38: anonymous fn/2 in AppWeb.SendLive.render/1
    (phoenix_live_view 0.18.18) lib/phoenix_live_view/diff.ex:375: Phoenix.LiveView.Diff.traverse/7
    (phoenix_live_view 0.18.18) lib/phoenix_live_view/diff.ex:544: anonymous fn/4 in Phoenix.LiveView.Diff.traverse_dynamic/7
    (elixir 1.14.3) lib/enum.ex:2468: Enum."-reduce/3-lists^foldl/2-0-"/3
    (phoenix_live_view 0.18.18) lib/phoenix_live_view/diff.ex:373: Phoenix.LiveView.Diff.traverse/7
    (phoenix_live_view 0.18.18) lib/phoenix_live_view/diff.ex:139: Phoenix.LiveView.Diff.render/3
    (phoenix_live_view 0.18.18) lib/phoenix_live_view/channel.ex:833: Phoenix.LiveView.Channel.render_diff/3
    (phoenix_live_view 0.18.18) lib/phoenix_live_view/channel.ex:689: Phoenix.LiveView.Channel.handle_changed/4
    (stdlib 4.2) gen_server.erl:1123: :gen_server.try_dispatch/4
    (stdlib 4.2) gen_server.erl:1200: :gen_server.handle_msg/6
    (stdlib 4.2) proc_lib.erl:240: :proc_lib.init_p_do_apply/3
Last message: {:text_stuff, %{"text" => "ad"}}
State: %{components: {%{}, %{}, 1}, join_ref: "4", serializer: Phoenix.Socket.V2.JSONSerializer, socket: #Phoenix.LiveView.Socket<id: "phx-F3-xORYdOS-9swdH", endpoint: AppWeb.Endpoint, view: AppWeb.SendLive, parent_pid: nil, root_pid: #PID<0.1420.0>, router: AppWeb.Router, assigns: %{__changed__: %{}, flash: %{}, live_action: :home, message_item: ""}, transport_pid: #PID<0.1413.0>, ...>, topic: "lv:phx-F3-xORYdOS-9swdH", upload_names: %{}, upload_pids: %{}}`

Full LIve VIew Page

defmodule AppWeb.SendLive do
  use AppWeb, :live_view
  

  def mount(_params, _session, socket) do

    if connected?(socket) do
      Phoenix.PubSub.subscribe(App.PubSub, topic)    
    end

    {:ok, assign(socket, message_item: "")}
  end

    def handle_info({:text_stuff, text}, socket) do 
    {:noreply, assign(socket, message_item: text)}
  end

  def handle_event("send", %{"text" => text}, socket) do     # This is what I changed 
    IO.inspect text
    Phoenix.PubSub.broadcast(App.PubSub, topic, {:text_stuff, text}) 
    {:noreply, socket}
  end
  
  defp topic do #Topic
    "message"
  end

  def render(assigns)do   
   ~H"""
    <div>
      <h1>Send Message</h1>
      <form phx-submit="send">
        <input type="text" name="text" />
        <button type="submit">Send</button>
      </form>
     <div>
      <h1>ChatLive</h1>
      <%= @message_item %>
      </div>

  
    </div>

    """
  end
end



Sure! So when I read this I see that the top line is:

** (Protocol.UndefinedError) protocol Phoenix.HTML.Safe not implemented for %{"text" => "ad"} of type Map. 

%{"text" => "ad"} sure looks like data from my app, so somewhere I’m giving Phoenix some data that isn’t HTML.Safe. Looking at the stack trace, the first few lines are in Phoenix code, but then I see

    (app 0.1.0) lib/app_web/live/send_live.ex:38: anonymous fn/2 in AppWeb.SendLive.render/1

That’s code from my app, so let’s go check it out. Line 38 is

<%= @message_item %>

This is where I’m injecting the @message_item assign into the HTML. The error says that it isn’t HTML safe, so basically it’s telling me it isn’t allowed to just plop an Elixir map into HTML. Looking at my code I can see where I set message_item which is just:

    def handle_info({:text_stuff, text}, socket) do 
      {:noreply, assign(socket, message_item: text)}
    end

So somehow text is actually %{"text" => "ad"}. From there I’d look and see at what sends {:text_stuff and so on.

3 Likes

Knowing that if I see Phoenix.HTML.Safe in an error it means some "bad " data was sent to the render is helpful.

Yeah Phoenix.HTML.Safe — Phoenix.HTML v3.3.2 is a bit terse but it does at least say:

In order to promote HTML safety, Phoenix templates do not use Kernel.to_string/1 to convert data types to strings in templates. Instead, Phoenix uses this protocol which must be implemented by data structures and guarantee that a HTML safe representation is returned.

The key bit is “to convert data types to strings in templates”. So if you get a ** (Protocol.UndefinedError) protocol Phoenix.HTML.Safe not implemented for error then that means it couldn’t convert it to a string in a template.

1 Like

You should probably use guard clause, as text here can be anything

The error would be no match clause if You check text with guards, but You pass something with wrong value

If You are not sure, You can always always render with…

<%= inspect @message_item %>
2 Likes