How to notify a user about exception in LiveView handle_event?

What happens: User opens LiveView page and clicks a button. LiveView handle_event calls a function that crashes. LiveView restarts. User is unaware that something went wrong

What I expect to happen: user is informed that something went wrong. Classic controllers show the 500 error page in similar case.

Error and exception handling doc says

If the error happens during an event, the LiveView process will crash. The client will notice the error and remount the LiveView - without reloading the page. This is enough to update the page and show the user the latest information.

But nothing on how to track this and notify user. I guess I could wrap each function call in try/rescue block and issue a notification myself, but I hope there is a better way

I’d appreciate an advice

Inside handle_event you need to also handle the “bad” result(s) from your function call, usually with a case statement. For example:

def handle_event("some_event", params, socket) do
  case function_with_multiple_results(params) do
    good_result ->
      socket = intended_effects(socket)
      {:noreply, socket}

    bad_result ->
      socket = show_my_error_page(socket)
      {:noreply, socket}
  end
end

Display the error message on the view itself rather than sending them to a new page?

<% if @error do %>
<div class="error">You don't have permissions<div>
</div>
def handle_event("some_event", params, socket) do
  case function_with_multiple_results(params) do
    good_result ->
      socket = intended_effects(socket)
      {:noreply, socket}

    bad_result ->
      socket = assign(socket, error: "my error")
      {:noreply, socket}
  end
end

or

socket |> put_flash(socket, :info, "It worked!")
socket |> put_flash(socket, :error, "You can't access that page")

There might be a case when intended_effects() unexpectedly raises – due to a bug for example. In this case LV will just go back to mount. And a user has no idea that something went wrong, other than no changes on the page.

I’d expect LV to know about that. At least it knows if raise happens in mount() and shows the error page.

In this scenario, the LiveView “goes back to mount” because it crashed and was replaced by a new LiveView, which knows nothing about the old one.

If you’re going to go through the effort of reporting the crash, you might as well just fix the error to not crash the LiveView! Within intended_effects() just like in handle_event you should have all possible cases accounted for (this is the standard in a functional language). If there is an error case, a changeset should be returned containing the errors and these errors displayed in the LV. It should never result in a LV crash.

Are you using Ecto to handle your data? Generally the Ecto callbacks return {:ok, struct} on success, and {:error, changeset} on failure. Then you can wrap them in a case statement like shown earlier:

case Accounts.update_user_password(user, password, user_params) do
  {:ok, _user} ->
    socket = put_flash(socket, :info, "Password updated successfully.")
    {:noreply, socket}

  {:error, changeset} ->
    socket = assign(socket, password_changeset: changeset)
    {:noreply, socket}
end

and display the errors to the user (here the example is for a password reset form)

<%= f = form_for @password_changeset, "#", phx_submit: "change_password" %>

  <%= if @password_changeset.action do %>
    <div class="alert alert-danger">
      <p>Oops, something went wrong! Please check the errors below.</p>
    </div>
  <% end %>

  <%= label f, :password, "New password" %>
  <%= password_input f, :password %>
  <%= error_tag f, :password %>

  <%= label f, :current_password, "Current password" %>
  <%= password_input f, :current_password %>
  <%= error_tag f, :current_password %>

  <%= submit "Change password" %>
</form>

I understand that the best approach is to produce bugs free code and expect all possible outcomes from functions. But bugs happen. Things go wrong. Not all things are as simple as Repo.update. There is always a lag between error reported and error fixed during which users face the error.

Seeing 500 page is no good. But even worse is not seeing it when something went wrong because then user has no idea if a function worked or not.

What I don’t understand is that exceptions on mount are caught and converted to an exception page by Phoenix error views - pretty much like the way it works with controllers - quote from LV docs. But exceptions on handle_event are not converted to error view. So if exception happens on mount - the user knows that there is a server error. If exception happens after a button click - the user is unaware, because the page stays the same.

2 Likes

This isn’t exactly accurate. In some sense, this is less about mount vs handle_event, and more about the static vs live interactions. Crashes in any of the functions called statically are part of the plug pipeline (mount, handle_params) result in plug handling the error and then you get a 500. Those same functions (and any other functions) when called during the live render crash the process, and then the front end tries to reconnect.

1 Like

It’s about user experience with the LiveView. If raise happens on mount the error page is rendered. If raise happens on handle_event - the error page is not rendered, and user is unaware that something went wrong, LiveView silently restarts without refreshing the page.

I’m looking for a way to let the user know that there was an error. Ideally without having to wrap all calls into try/rescue. So far I was unable to find anything in the docs

Both 500 and restarts are bad. In liveview, restart is simpler to implement, and in “deadview” 500 is simpler to implement. So what you are seeing here as discrepancy is the library making the least amount of effort when the developer’s intention is unknown.

1 Like

I agee

I’m not looking for discrepancies. I’m looking for an answer to the question: How to notify a user about exception in the LiveView handle_event? I still hope there is a solution better than wrapping function calls with try/catch.

It should be possible to make a safe_handle_event wrapper macro to do that. Someone better than me in meta-programming can step up to take the challenge?

def already supports the following form:

def handle_event("event", args, socket) do
  # whatever
rescue
 e ->
  # handle exception here
end

This lets you skip the boilerplate of an additional try do end

2 Likes

But if I have many handle_event/3 clauses, I still need to duplicate the rescue part. @Athunnea just needs a standard way to redirect to a known 500 page if anything bad happen.

1 Like

assuming your LV does crash then on JavaScript side doing this

liveSocket.channel.onError(thing)

should get you a trigger that works

3 Likes

Uncaught TypeError: liveSocket.channel.onError is not a function

I don’t think live socket exposes the same interface as the normal socket.

AFAIK, each view has it’s own channel (?), you might be able to hook into each views create/mount call back and use that to bind an “error” handler with the view name (aka channel name?), but I believe the “event” passed around here is more like “phx_update”, not a socket error. I didn’t read too hard or actually test it.

Edit: this.viewName is more like MyApp.View, you need this.__view which is definitely getting weedsy and unsupported, onChannel is marked private too so not recommended to build on.

I think it would be a good feature to have, an easy way to bind per-view error handlers. Maybe another client hook lifecycle callback or a param to destroyed indicating if it was a clean exit or not.

That would probably need the core LV to be patched with the rescue block I guess.

1 Like