View Source Error and exception handling

As with any other Elixir code, exceptions may happen during the LiveView life-cycle. This page describes how LiveView handles errors at different stages.

Expected scenarios

In this section, we will talk about error cases that you expect to happen within your application. For example, a user filling in a form with invalid data is expected. In a LiveView, we typically handle those cases by storing the form state in LiveView assigns and rendering any relevant error message back to the client.

We may also use flash messages for this. For example, imagine you have a page to manage all "Team members" in an organization. However, if there is only one member left in the organization, they should not be allowed to leave. You may want to handle this by using flash messages:

if MyApp.Org.leave(socket.assigns.current_org, member) do
  {:noreply, socket}
else
  {:noreply, put_flash(socket, :error, "last member cannot leave organization")}
end

However, one may argue that, if the last member of an organization cannot leave it, it may be better to not even show the "Leave" button in the UI when the organization has only one member.

Given the button does not appear in the UI, triggering the "leave" action when the organization has only one member is an unexpected scenario. This means we can rewrite the code above to:

true = MyApp.Org.leave(socket.assigns.current_org, member)
{:noreply, socket}

If leave does not return true, Elixir will raise a MatchError exception. Or you could provide a leave! function that raises a specific exception:

MyApp.Org.leave!(socket.assigns.current_org, member)
{:noreply, socket}

However, what will happen with a LiveView in case of exceptions? Let's talk about unexpected scenarios.

Unexpected scenarios

Elixir developers tend to write assertive code. This means that, if we expect leave to always return true, we can explicitly match on its result, as we did above:

true = MyApp.Org.leave(socket.assigns.current_org, member)
{:noreply, socket}

If leave fails and returns false, an exception is raised. It is common for Elixir developers to use exceptions for unexpected scenarios in their Phoenix applications.

For example, if you are building an application where a user may belong to one or more organizations, when accessing the organization page, you may want to check that the user has access to it like this:

organizations_query = Ecto.assoc(socket.assigns.current_user, :organizations)
Repo.get!(organizations_query, params["org_id"])

The code above builds a query that returns all organizations that belongs to the current user and then validates that the given org_id belongs to the user. If there is no such org_id or if the user has no access to it, Repo.get! will raise an Ecto.NoResultsError exception.

During a regular controller request, this exception will be converted to a 404 exception and rendered as a custom error page, as detailed here. LiveView will react to exceptions in three different ways, depending on where it is in its life-cycle.

Exceptions during HTTP mount

When you first access a LiveView, a regular HTTP request is sent to the server and processed by the LiveView. The mount callback is invoked and then a page is rendered. Any exception here is caught, logged, and converted to an exception page by Phoenix error views - exactly how it works with controllers too.

Exceptions during connected mount

If the initial HTTP request succeeds, LiveView will connect to the server using a stateful connection, typically a WebSocket. This spawns a long-running lightweight Elixir process on the server, which invokes the mount callback and renders an updated version of the page.

An exception during this stage will crash the LiveView process, which will be logged. Once the client notices the crash, it fully reloads the page. This will cause mount to be invoked again during a regular HTTP request (the exact scenario of the previous subsection).

In other words, LiveView will reload the page in case of errors, making it fail as if LiveView was not involved in the rendering in the first place.

Exceptions after connected mount

Once your LiveView is mounted and connected, any error will cause the LiveView process to crash and be logged. Once the client notices the error, it will remount the LiveView over the stateful connection, without reloading the page (the exact scenario of the previous subsection). If remounting succeeds, the LiveView goes back to a working state, updating the page and showing the user the latest information.

For example, let's say two users try to leave the organization at the same time. In this case, both of them see the "Leave" button, but our leave function call will succeed only for one of them:

true = MyApp.Org.leave(socket.assigns.current_org, member)
{:noreply, socket}

When the exception raises, the client will remount the LiveView. Once you remount, your code will now notice that there is only one user in the organization and therefore no longer show the "Leave" button. In other words, by remounting, we often update the state of the page, allowing exceptions to be automatically handled.

Note that the choice between conditionally checking on the result of the leave function with an if, or simply asserting it returns true, is completely up to you. If the likelihood of everyone leaving the organization at the same time is low, then you may as well treat it as an unexpected scenario. Although other developers will be more comfortable by explicitly handling those cases. In both scenarios, LiveView has you covered.

Finally, if your LiveView crashes, its current state will be lost. Luckily, LiveView has a series of mechanisms and best practices you can follow to ensure the user is shown the same page as before during reconnections. See the "Deployments and recovery" guide for more information.