Not understanding how to handle failures from assign_async

The async-assigns docs say the assign_async function should return {:ok, assigns} or {:error, reason}. I can’t find any docs showing how to digest these in heex, but did find this blogpost saying to use them this way:

  <.async_result :let={user} assign={@tim}>
    <:loading>Loading Tim...</:loading>
    <:failed :let={reason}><%= reason %></:failed>

    <span :if={user}><%= %></span>

But when I try to do this, my liveview crashes and restarts:

(Protocol.UndefinedError) protocol Phoenix.HTML.Safe not implemented for {:error, "ZERO_RESULTS"}

So it looks like it’s trying to render the full tuple, instead of just being passed the reason.

My code:

  def handle_event("search_places", %{"search" => %{"query" => query}}, socket) do
     |> assign(:search_results, AsyncResult.loading())
     |> assign_async(:search_results, fn ->
       case find(query) do
         {:ok, %{"results" => results}} ->
           {:ok, %{search_results: results}}

         {:error, error} ->
           dbg(error) # outputs "ZERO_RESULTS"
           Logger.warning("Error autocompleting places: #{inspect(error)}")
           {:error, error}

        <.async_result :let={search_results} assign={@search_results}>
            <div class="w-full text-gray-500 text-center pt-4">Loading...</div>
          <:failed :let={reason}><%= reason %></:failed>
              :for={result <- search_results}
                <%= result %>

What am I missing here?


Found the LiveView.html docs of this, which don’t specifically show how to use the reason:

<.async_result :let={org} assign={@org}>
  <:loading>Loading organization...</:loading>
  <:failed :let={_reason}>there was an error loading the organization</:failed>
  <%= %>

So do I need to pattern match in the :failed slot?

<:failed :let={{:error, reason}}><%= reason %></:failed>

This line:

<:failed :let={reason}><%= reason %></:failed>

should be:

<:failed :let={{:error, reason}}><%= reason %></:failed>

…at least that is what it seems according to the error. I haven’t played with async assigns yet!


This is correct.

However, you probably want to use a bang! Function that will raise in the event of an error so that the result helper will use the failed slot with just {reason}

I can update the post to mention this pit fall

It’s confusing because the success case removes the :ok while the failure does not

1 Like

Yep, that’s it! You figured it out while I was typing up an edit. Thanks!

1 Like

Yeah, that’s pretty confusing. It seems like you’d want the same behavior for success and failure- stripping the atom and just returning the content.

I opened PR#2991 to try to clarify the docs around the usage, though now realizing based on the handling of unhandled errors, that my doc fix isn’t right either.

Thanks all!

I would just submit the PR yourself! All you really need to do is change :let={_reason} to :let={{:error, _reason}} and that should be clear enough.

I did open a PR, though it was (correctly) closed since I incorrectly assumed it could always use the let={{:error, reason}} syntax.

Oh my bad, my brain auto-corrected “PR” to “issue” somehow.

That makes sense. I thought it was probably something like that but the docs don’t touch on this and I agree it’s a bit confusing.

I ended up creating a new component to try to deal with the many results that can come out of the :failure slot:

  attr :failure, :any, required: true
  attr :fallback, :string, default: "Unknown error"

  def failure_display(assigns) do
    assigns = assign(assigns, :message, failure_to_string(assigns.failure))

    <div class="w-full text-gray-500 text-center pt-4">
      <%= @message || @fallback %>

  def failure_to_string(reason) when is_binary(reason), do: reason
  def failure_to_string({:error, reason}), do: failure_to_string(reason)
  def failure_to_string({:exit, reason}), do: failure_to_string(reason)
  def failure_to_string(_), do: nil