javascript 89 lines · 3 tabs

Turbo Drive lifecycle: attach global error handler

Shared by codesnips Jan 2026
3 tabs
let installed = false;

function notify(message, level = "error") {
  document.dispatchEvent(
    new CustomEvent("flash:show", { detail: { message, level } })
  );
}

function onBeforeFetchResponse(event) {
  const { fetchResponse } = event.detail;
  const status = fetchResponse.response.status;

  if (status >= 500) {
    event.preventDefault();
    notify("Something went wrong on our end. Please try again.");
  } else if (status === 429) {
    event.preventDefault();
    notify("You're going too fast — slow down for a moment.", "warning");
  }
}

function onFetchRequestError(event) {
  event.preventDefault();
  notify("Network error. Check your connection and retry.");
}

function onLoad() {
  document.dispatchEvent(new CustomEvent("flash:clear"));
}

export function installTurboErrorHandler() {
  if (installed) return;
  installed = true;

  document.addEventListener("turbo:before-fetch-response", onBeforeFetchResponse);
  document.addEventListener("turbo:fetch-request-error", onFetchRequestError);
  document.addEventListener("turbo:load", onLoad);
}

installTurboErrorHandler();
3 files · javascript Explain with highlit

Turbo Drive turns a server-rendered Rails app into an SPA-like experience by intercepting link clicks and form submissions, then fetching and swapping <body> over the network. Because navigation happens through fetch rather than a full browser page load, the usual browser affordances — the error page for a failed request, the loading spinner in the tab — are bypassed. This snippet wires up a small runtime that reattaches those affordances by listening to the Turbo Drive lifecycle events.

In turbo_error_handler.js, the core idea is that Turbo dispatches DOM events at each stage of a visit. turbo:before-fetch-response fires with the raw fetchResponse before the DOM is touched, which is the right moment to inspect response.status. Any status at or above 500 is treated as a hard failure: the handler stops the default rendering with event.preventDefault() so Turbo does not swap in an error HTML page, and instead surfaces a toast. A turbo:fetch-request-error listener covers the case where the network itself fails and no response arrives at all. turbo:load is used to clear transient state once a page successfully renders.

A subtle point is idempotency: Turbo emits turbo:load on every visit, so the module guards attachment with an installed flag to avoid registering duplicate listeners across navigations. The handler is deliberately attached to document once at import time rather than inside a controller.

In flash_controller.js, a Stimulus controller renders the toast the handler asks for. It exposes show via a small custom event bridge so plain modules can talk to Stimulus without holding a reference to the controller instance, and it auto-dismisses using a setTimeout cleared in disconnect to prevent leaks when Turbo tears down the element.

application.js shows the wiring order that matters: Turbo and Stimulus start first, then the error handler is imported for its side effect. The trade-off of this pattern is that global handlers are coarse — they cannot easily know which UI triggered a request — so it favors broad resilience over per-action messaging. It is the right reach when an app needs consistent failure feedback across many Turbo visits without sprinkling try/catch everywhere.


Related snips

ruby
class CommentsController < ApplicationController
  before_action :set_post

  def create
    @comment = @post.comments.build(comment_params)

System test: asserting Turbo Stream responses

rails hotwire turbo
by codesnips 4 tabs
ruby
class Post < ApplicationRecord
  belongs_to :author, class_name: 'User'
  has_many :comments, dependent: :destroy

  scope :published, -> { where.not(published_at: nil).where('published_at <= ?', Time.current) }
  scope :draft, -> { where(published_at: nil) }

ActiveRecord scopes for reusable query logic

rails activerecord patterns
by Alex Kumar 1 tab
ruby
module Api
  module V1
    class UsersController < BaseController
      def show
        user = User.includes(:profile).find(params[:id])

ETags for conditional requests and caching

rails caching http-caching
by Alex Kumar 1 tab
ruby
class PostsController < ApplicationController
  def index
    @posts = Post.includes(:author)
                 .order(created_at: :desc)
                 .page(params[:page])
                 .per(10)

Turbo Frames: infinite scroll with lazy-loading frame

rails turbo hotwire
by codesnips 4 tabs
ruby
require "csv"

class PeopleCsvStream
  include Enumerable

  HEADERS = %w[id full_name email signed_up_at plan].freeze

Resilient CSV Export as a Streamed Response

rails performance streaming
by codesnips 3 tabs
erb
<form data-controller="query-sync" data-action="change->query-sync#apply">
  <select name="status" class="rounded border p-2">
    <option value="">Any</option>
    <option value="open">Open</option>
    <option value="closed">Closed</option>
  </select>

Filter UI that syncs query params via Stimulus (no front-end router)

rails hotwire stimulus
by Henry Kim 2 tabs

Share this code

Here's the card — post it anywhere.

Turbo Drive lifecycle: attach global error handler — share card
Link copied