> ## Content Index
> Fetch the complete content index at: https://til.codes/llms.txt
> Use this file to discover other available public pages before exploring further.

# LiveView Already Knows When Your Server Crashed
- URL: https://til.codes/liveview-already-knows-when-your-server-crashed/
- Published: 2026-04-03T03:18:56.000Z
- Updated: 2026-04-03T03:18:56.000Z
- Author: Manu Ajith
- Tags: elixir, phoenix

This week, I reviewed a pull request that added error flash handling to a Phoenix app. The JavaScript implementation seemed straightforward: listening for `phx:page-loading-start`, checking the socket connection, showing a popover for server crashes, and hiding it during navigation. It included two helper functions, an event listener, and a manual `isConnected()` check to differentiate between server errors and network issues. Then I opened the LiveView documentation and realized we were reinventing functionality that already exists in the framework.

### What We Initially Built

Here's the code we thought was complete:

```javascript
window.addEventListener("phx:page-loading-start", (info) => {
  topbar.show(300)

  if (info.detail.kind === "error" && liveSocket.socket.isConnected()) {
    showErrorFlash()
  } else {
    clearErrorFlash()
  }
})

function showErrorFlash() {
  const el = document.getElementById("client-error-flash")
  if (el && !el.matches(":popover-open")) {
    el.showPopover()
  }
}

function clearErrorFlash() {
  const el = document.getElementById("client-error-flash")
  if (el?.matches(":popover-open")) {
    el.hidePopover()
  }
}

```

The key logic here is the `isConnected()` check. The `phx:page-loading-start` event triggers with `kind: "error"` for both server crashes and network disconnects. To show the error flash only for server crashes, we checked if the socket was connected at the time of the error, an indicator that the server process failed rather than the network. It worked, and initially, it made sense. But as I dug deeper into the docs, I realized LiveView already accounts for this distinction.

### LiveView's Built-in Classes

LiveView uses [CSS classes on the root container](https://hexdocs.pm/phoenix%5Flive%5Fview/syncing-changes.html?ref=til.codes#navigation-classes) to represent connection states. The `data-phx-main` element gets the following classes:

- `phx-connected` when the WebSocket is active.
- `phx-disconnected` when the network connection drops.
- `phx-error` when the server process crashes.

That last class, `phx-error`, was exactly what we needed. LiveView distinguishes between server crashes and network disconnects, applying `phx-error` only to server issues. The manual `isConnected()` check and event logic we wrote were redundant.

### Observing Classes Instead of Events

If we were just toggling a `div` CSS alone could handle this: `.phx-error #client-error-flash { display: block }`. But since we're using the [Popover API](https://developer.mozilla.org/en-US/docs/Web/API/Popover%5FAPI?ref=til.codes) with `popover="manual"`, CSS can't do it alone. The popover needs to be in the top layer to properly display above `<dialog>` modals in our app. We simplified the JavaScript by replacing the event listener and helper functions with a `MutationObserver` to watch for class changes on the LiveView container:

```javascript
const main = document.querySelector("[data-phx-main]")
if (main) {
  new MutationObserver(() => {
    const flash = document.getElementById("client-error-flash")
    if (!flash) return

    if (main.classList.contains("phx-error")) {
      if (!flash.matches(":popover-open")) flash.showPopover()
    } else {
      if (flash.matches(":popover-open")) flash.hidePopover()
    }
  }).observe(main, { attributes: true, attributeFilter: ["class"] })
}

```

The observer reacts to LiveView's state transitions. When a server crash occurs, `phx-error` is added, triggering the popover to open. When LiveView reconnects, `phx-error` is replaced with `phx-connected`, closing the popover. For network disconnects, `phx-disconnected` is added instead, and the observer does nothing, exactly the behavior we wanted, but with much less code.

### Timing Considerations

One issue we ran into: the observer must be set up **after** `liveSocket.connect()`. Initially, we placed it at the top of the script, but `querySelector` returned `null` because the `[data-phx-main]` element wasn't ready. Moving the setup after `liveSocket.connect()` resolved this:

```javascript
liveSocket.connect()

const main = document.querySelector("[data-phx-main]")
if (main) {
  new MutationObserver(() => {
    // ...
  }).observe(main, { attributes: true, attributeFilter: ["class"] })
}

```

Alternatively, a `phx-hook` guarantees the DOM is ready, but for a simple observer like this, the standalone approach is cleaner.

### The Bigger Takeaway

This reinforced a lesson I've learned before: check what LiveView already provides before writing custom JavaScript. Features like `phx:page-loading-start` events and navigation classes are well-documented, but it's easy to default to event-driven logic because that's what most JavaScript frameworks emphasize. LiveView often does more than you expect. Sometimes, the best solution is to rely on what's already built in.

**Further reading:**

- [LiveView navigation classes](https://hexdocs.pm/phoenix%5Flive%5Fview/syncing-changes.html?ref=til.codes#navigation-classes)
- [Popover API](https://developer.mozilla.org/en-US/docs/Web/API/Popover%5FAPI?ref=til.codes)
- [MutationObserver](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver?ref=til.codes)