T-CREATOR

htmx のエラーハンドリングとデバッグのコツ

 htmx のエラーハンドリングとデバッグのコツ

モダンな Web アプリケーション開発において、ユーザーエクスペリエンスを向上させるためには適切なエラーハンドリングが欠かせません。特に HTMX を使用したアプリケーションでは、従来の JavaScript とは異なる考え方でエラー処理を行う必要があります。

HTMX は HTML 属性を使って Ajax 通信や DOM 操作を行う革新的なライブラリですが、その特性上、エラーが発生した際の対処法や デバッグ手法が従来の JavaScript アプリケーションとは大きく異なります。適切なエラー処理を実装しないと、ユーザーが何が起こっているかわからない状況に陥ったり、アプリケーションが予期しない動作をしたりする可能性があります。

本記事では、HTMX アプリケーションにおけるエラーハンドリングの実践的な手法と、効率的なデバッグのコツについて詳しく解説いたします。初心者の方でも実装できるよう、具体的なコード例とともに段階的にご紹介していきますので、ぜひ最後までご覧ください。

背景

HTMX におけるエラーの種類

HTMX アプリケーションで発生するエラーは、大きく分けて以下の 4 つのカテゴリに分類されます。

#エラーカテゴリ発生原因主な症状
1ネットワークエラー接続障害、タイムアウトリクエストが完了しない
2HTTP ステータスエラー4xx、5xx エラーコードエラーレスポンスの受信
3レスポンス形式エラー不正な HTML、JSON表示の崩れ、動作不良
4設定・属性エラーhx-*属性の誤設定期待した動作をしない

これらのエラーは、HTMX の動作原理を理解することで効率的に対処できます。以下の図で、HTMX におけるリクエスト・レスポンスのフローとエラー発生ポイントを確認しましょう。

mermaidflowchart TD
  user[ユーザー操作] -->|クリック/送信| trigger[hx-*属性のトリガー]
  trigger -->|HTTPリクエスト| server[サーバー]
  server -->|正常レスポンス| success[HTML受信]
  server -->|エラーレスポンス| error_resp[4xx/5xxエラー]

  trigger -->|ネットワーク障害| network_error[ネットワークエラー]
  success -->|DOM更新| dom_update[DOM置換・挿入]
  success -->|形式エラー| format_error[レスポンス形式エラー]

  error_resp --> handle_error[エラーハンドリング]
  network_error --> handle_error
  format_error --> handle_error

  handle_error -->|適切な処理| user_feedback[ユーザーへのフィードバック]

図で理解できる要点:

  • エラーはリクエスト送信から DOM 更新まで複数のポイントで発生する
  • 各エラーポイントに応じた適切な処理が必要
  • 最終的にはユーザーにわかりやすいフィードバックを提供することが重要

従来の JavaScript との違い

HTMX のエラーハンドリングは、従来の JavaScript(fetch API や XMLHttpRequest)とは根本的に異なるアプローチを取ります。

従来の JavaScript(fetch API)の場合

javascript// 従来のfetch APIでのエラーハンドリング
async function fetchData() {
  try {
    const response = await fetch('/api/data');

    if (!response.ok) {
      throw new Error(
        `HTTP error! status: ${response.status}`
      );
    }

    const data = await response.json();
    // DOM操作でデータを表示
    document.getElementById('result').innerHTML = data.html;
  } catch (error) {
    // エラー処理
    console.error('Fetch error:', error);
    document.getElementById('error').textContent =
      'エラーが発生しました';
  }
}

HTMX の場合

html<!-- HTMXでのエラーハンドリング -->
<div
  id="content"
  hx-get="/api/data"
  hx-trigger="click"
  hx-target="#result"
  hx-on::response-error="handleError(event)"
  hx-on::network-error="handleNetworkError(event)"
>
  データを取得
</div>

<div id="result"></div>
<div id="error-message" style="display:none;"></div>
javascript// HTMXのイベントハンドラー
function handleError(event) {
  const errorDiv = document.getElementById('error-message');
  errorDiv.textContent = `サーバーエラーが発生しました (${event.detail.xhr.status})`;
  errorDiv.style.display = 'block';
}

function handleNetworkError(event) {
  const errorDiv = document.getElementById('error-message');
  errorDiv.textContent = 'ネットワークエラーが発生しました';
  errorDiv.style.display = 'block';
}

この比較から、以下の違いが明らかになります:

項目従来の JavaScriptHTMX
エラー検出try-catch 文で例外をキャッチイベントリスナーで HTMX イベントを処理
エラー情報response.status や error.messageevent.detail.xhr からアクセス
DOM 操作明示的に DOM 要素を操作hx-target 属性で自動的に更新
コード記述量多い(fetch + DOM 操作)少ない(HTML 属性中心)

課題

よくある HTMX エラー

HTMX アプリケーションの開発において、開発者がよく遭遇するエラーパターンをご紹介します。これらのエラーを事前に知っておくことで、トラブルシューティングが格段に効率化されます。

1. 404 Not Found エラー

最も頻繁に発生するエラーの一つです。

html<!-- 問題のあるコード例 -->
<button hx-get="/api/user/detail" hx-target="#user-info">
  ユーザー詳細を表示
</button>

エラーコード: HTTP 404: Not Found エラーメッセージ: GET ​/​api​/​user​/​detail 404 (Not Found)

発生条件:

  • エンドポイント URL の記述ミス
  • サーバー側でルーティングが設定されていない
  • パスパラメータの不備

2. CORS(Cross-Origin Resource Sharing)エラー

異なるドメインへのリクエスト時に発生します。

エラーコード: CORS policy: No 'Access-Control-Allow-Origin' header エラーメッセージ:

csharpAccess to fetch at 'https://api.example.com/data' from origin 'http://localhost:3000'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present
on the requested resource.

3. レスポンスタイムアウトエラー

長時間かかる処理でタイムアウトが発生する場合です。

html<!-- タイムアウトが発生しやすい例 -->
<form hx-post="/api/heavy-processing" hx-target="#result">
  <!-- 重い処理を実行するフォーム -->
</form>

エラーコード: TimeoutError エラーメッセージ: Request timeout after 120000ms

4. JSON/HTML レスポンス形式エラー

期待する形式と異なるレスポンスが返される場合です。

javascript// サーバー側で誤ってJSONを返している例
app.get('/api/fragment', (req, res) => {
  res.json({ error: 'User not found' }); // HTMLを期待しているのにJSONが返る
});

デバッグが困難な理由

HTMX アプリケーションのデバッグが困難になる主な理由を分析してみましょう。

1. HTML ベースの宣言的記述

HTMX は HTML 属性でロジックを記述するため、JavaScript のように明示的なエラーハンドリングコードがない場合が多く、問題の発生箇所を特定しにくくなります。

html<!-- どこでエラーが発生しているか分かりにくい例 -->
<div
  hx-get="/api/data"
  hx-trigger="intersect"
  hx-target="#content"
  hx-swap="innerHTML"
  hx-select=".main-content"
></div>

2. 非同期処理の可視性不足

従来の JavaScript と異なり、HTMX の非同期処理はバックグラウンドで実行されるため、処理の状況が見えにくいという課題があります。

mermaidsequenceDiagram
  participant User as ユーザー
  participant Browser as ブラウザ
  participant HTMX as HTMX
  participant Server as サーバー

  User->>Browser: ボタンクリック
  Browser->>HTMX: hx-get実行
  Note over HTMX,Server: この間の処理が見えない
  HTMX->>Server: HTTPリクエスト
  Server-->>HTMX: レスポンス/エラー
  HTMX->>Browser: DOM更新
  Browser->>User: 結果表示

3. エラーイベントの理解不足

HTMX は独自のイベントシステムを持っており、適切なイベントをリスンしないとエラー情報を取得できません。

主要な HTMX エラーイベント:

  • htmx:responseError: HTTP エラーレスポンス時
  • htmx:sendError: リクエスト送信エラー時
  • htmx:timeout: タイムアウト時
  • htmx:aborted: リクエストがキャンセルされた時

解決策

エラーハンドリングの基本戦略

HTMX アプリケーションにおける効果的なエラーハンドリング戦略をご紹介します。段階的なアプローチにより、堅牢なアプリケーションを構築できます。

1. グローバルエラーハンドリングの設定

まず、アプリケーション全体でのエラーハンドリングを設定します。

javascript// アプリケーション全体のエラーハンドリング設定
document.addEventListener('DOMContentLoaded', function () {
  // HTTPエラーレスポンスの処理
  document.body.addEventListener(
    'htmx:responseError',
    function (event) {
      const status = event.detail.xhr.status;
      const target = event.detail.target;

      console.error(
        `HTTP Error ${status} for request to ${event.detail.requestConfig.path}`
      );

      // ステータスコード別の処理
      switch (status) {
        case 400:
          showErrorMessage(
            target,
            'リクエストに問題があります'
          );
          break;
        case 401:
          showErrorMessage(target, '認証が必要です');
          // ログインページにリダイレクト
          window.location.href = '/login';
          break;
        case 403:
          showErrorMessage(
            target,
            'アクセス権限がありません'
          );
          break;
        case 404:
          showErrorMessage(
            target,
            '要求されたリソースが見つかりません'
          );
          break;
        case 500:
          showErrorMessage(
            target,
            'サーバーエラーが発生しました'
          );
          break;
        default:
          showErrorMessage(target, 'エラーが発生しました');
      }
    }
  );
});

2. ネットワークエラーの処理

ネットワーク接続に関するエラーを処理します。

javascript// ネットワークエラーハンドリング
document.body.addEventListener(
  'htmx:sendError',
  function (event) {
    const target = event.detail.target;
    console.error('Network error:', event.detail);

    showErrorMessage(
      target,
      'ネットワーク接続に問題があります。しばらくしてから再度お試しください。'
    );

    // リトライ機能の追加
    addRetryButton(target, event.detail.requestConfig);
  }
);

3. ユーザビリティを向上させるエラー表示

エラーメッセージを表示するヘルパー関数を実装します。

javascript// エラーメッセージ表示のヘルパー関数
function showErrorMessage(target, message) {
  // エラーメッセージ用の要素を作成
  const errorDiv = document.createElement('div');
  errorDiv.className = 'error-message';
  errorDiv.innerHTML = `
    <div class="alert alert-danger" role="alert">
      <strong>エラー:</strong> ${message}
      <button type="button" class="btn-close" aria-label="閉じる" onclick="this.parentElement.remove()"></button>
    </div>
  `;

  // 既存のエラーメッセージがあれば削除
  const existingError = target.querySelector(
    '.error-message'
  );
  if (existingError) {
    existingError.remove();
  }

  // 新しいエラーメッセージを挿入
  target.insertBefore(errorDiv, target.firstChild);
}

// リトライボタンを追加する関数
function addRetryButton(target, requestConfig) {
  const retryButton = document.createElement('button');
  retryButton.className =
    'btn btn-outline-primary btn-sm mt-2';
  retryButton.textContent = '再試行';

  retryButton.addEventListener('click', function () {
    // 元のリクエストを再実行
    htmx.ajax(requestConfig.verb, requestConfig.path, {
      target: target,
      swap: requestConfig.swap,
    });
    retryButton.remove();
  });

  const errorDiv = target.querySelector('.error-message');
  if (errorDiv) {
    errorDiv.appendChild(retryButton);
  }
}

デバッグツールの活用法

HTMX アプリケーションの効率的なデバッグのために、以下のツールと手法を活用しましょう。

1. ブラウザ開発者ツールの活用

ブラウザの開発者ツールを効果的に使用する方法をご紹介します。

javascript// HTMXログ機能の有効化(開発環境用)
if (
  typeof htmx !== 'undefined' &&
  window.location.hostname === 'localhost'
) {
  htmx.logAll(); // 全てのHTMXイベントをコンソールに出力
}

// 詳細なデバッグ情報の出力
document.body.addEventListener(
  'htmx:beforeRequest',
  function (event) {
    console.log('HTMX Request:', {
      method: event.detail.requestConfig.verb,
      url: event.detail.requestConfig.path,
      target: event.detail.target,
      trigger: event.detail.requestConfig.triggeringEvent,
    });
  }
);

document.body.addEventListener(
  'htmx:afterRequest',
  function (event) {
    console.log('HTMX Response:', {
      status: event.detail.xhr.status,
      responseText:
        event.detail.xhr.responseText.substring(0, 200) +
        '...',
      target: event.detail.target,
    });
  }
);

2. カスタムデバッグヘルパーの実装

開発効率を向上させるためのデバッグヘルパーを実装します。

javascript// HTMXデバッグヘルパーオブジェクト
const HTMXDebugger = {
  // 現在アクティブなリクエストを追跡
  activeRequests: new Map(),

  // デバッグモードの有効化
  enable() {
    this.addRequestTracker();
    this.addErrorVisualizer();
    this.addPerformanceMonitor();
  },

  // リクエストトラッカーの追加
  addRequestTracker() {
    document.body.addEventListener(
      'htmx:beforeRequest',
      (event) => {
        const requestId = Date.now() + Math.random();
        this.activeRequests.set(requestId, {
          startTime: performance.now(),
          config: event.detail.requestConfig,
          target: event.detail.target,
        });

        // リクエスト中の視覚的インジケータ
        event.detail.target.classList.add('htmx-loading');
      }
    );

    document.body.addEventListener(
      'htmx:afterRequest',
      (event) => {
        event.detail.target.classList.remove(
          'htmx-loading'
        );
      }
    );
  },

  // エラー可視化の追加
  addErrorVisualizer() {
    document.body.addEventListener(
      'htmx:responseError',
      (event) => {
        const target = event.detail.target;
        target.style.border = '2px solid #dc3545';

        setTimeout(() => {
          target.style.border = '';
        }, 3000);
      }
    );
  },
};

// 開発環境でのみデバッグ機能を有効化
if (window.location.hostname === 'localhost') {
  HTMXDebugger.enable();
}

具体例

実際の Web アプリケーションでよく発生するエラーシナリオと、その対処法を具体的なコード例とともにご紹介します。

ネットワークエラーの処理

インターネット接続が不安定な環境や、サーバーが一時的にダウンした場合のネットワークエラー処理を実装してみましょう。

シナリオ: ユーザー情報取得時のネットワークエラー

html<!-- ユーザー情報を取得するボタン -->
<div class="user-info-container">
  <button
    id="fetch-user-btn"
    class="btn btn-primary"
    hx-get="/api/user/profile"
    hx-target="#user-profile"
    hx-indicator="#loading-spinner"
  >
    ユーザー情報を取得
  </button>

  <div id="loading-spinner" class="htmx-indicator">
    <span
      class="spinner-border spinner-border-sm"
      role="status"
    ></span>
    読み込み中...
  </div>

  <div id="user-profile"></div>
</div>

ネットワークエラーハンドリングの実装

javascript// ネットワークエラー処理の実装
function setupNetworkErrorHandling() {
  document.body.addEventListener(
    'htmx:sendError',
    function (event) {
      const target = event.detail.target;
      const errorContainer = createErrorContainer();

      // エラーメッセージの表示
      errorContainer.innerHTML = `
      <div class="alert alert-warning" role="alert">
        <i class="fas fa-exclamation-triangle"></i>
        <strong>接続エラー:</strong> ネットワークに接続できませんでした。
        <br><small>インターネット接続を確認してから再度お試しください。</small>
        <div class="mt-2">
          <button class="btn btn-sm btn-outline-primary retry-btn">
            <i class="fas fa-redo"></i> 再試行
          </button>
        </div>
      </div>
    `;

      // リトライ機能の実装
      const retryButton =
        errorContainer.querySelector('.retry-btn');
      retryButton.addEventListener('click', function () {
        // オフライン状態のチェック
        if (!navigator.onLine) {
          showOfflineMessage();
          return;
        }

        // 元のリクエストを再実行
        htmx.trigger(target, 'click');
        errorContainer.remove();
      });

      target.parentNode.insertBefore(
        errorContainer,
        target.nextSibling
      );
    }
  );
}

// オフライン状態のメッセージ表示
function showOfflineMessage() {
  const toast = document.createElement('div');
  toast.className = 'toast-notification';
  toast.innerHTML = `
    <div class="alert alert-info">
      <i class="fas fa-wifi"></i> 
      現在オフライン状態です。インターネット接続を確認してください。
    </div>
  `;

  document.body.appendChild(toast);

  // 5秒後に自動で削除
  setTimeout(() => toast.remove(), 5000);
}

サーバーエラーの処理

サーバー側で発生する 500 系エラーや、バリデーションエラーなどの 4xx 系エラーの処理方法をご紹介します。

シナリオ: フォーム送信時のサーバーエラー

html<!-- ユーザー登録フォーム -->
<form
  id="user-registration-form"
  hx-post="/api/users/register"
  hx-target="#form-result"
  hx-indicator="#form-spinner"
>
  <div class="mb-3">
    <label for="username" class="form-label"
      >ユーザー名</label
    >
    <input
      type="text"
      class="form-control"
      id="username"
      name="username"
      required
    />
  </div>

  <div class="mb-3">
    <label for="email" class="form-label"
      >メールアドレス</label
    >
    <input
      type="email"
      class="form-control"
      id="email"
      name="email"
      required
    />
  </div>

  <button type="submit" class="btn btn-success">
    <span
      id="form-spinner"
      class="htmx-indicator spinner-border spinner-border-sm"
    ></span>
    登録する
  </button>

  <div id="form-result"></div>
</form>

サーバーエラー処理の実装

javascript// サーバーエラー処理の実装
function setupServerErrorHandling() {
  document.body.addEventListener(
    'htmx:responseError',
    function (event) {
      const xhr = event.detail.xhr;
      const target = event.detail.target;
      const status = xhr.status;

      let errorMessage = '';
      let errorClass = '';

      try {
        // エラーレスポンスの内容を解析
        const errorResponse = JSON.parse(xhr.responseText);

        switch (status) {
          case 400: // Bad Request
            errorMessage =
              handleValidationErrors(errorResponse);
            errorClass = 'alert-warning';
            break;

          case 401: // Unauthorized
            errorMessage = `
            <i class="fas fa-lock"></i>
            <strong>認証エラー:</strong> ログインが必要です。
            <a href="/login" class="btn btn-sm btn-primary ms-2">ログイン</a>
          `;
            errorClass = 'alert-info';
            break;

          case 403: // Forbidden
            errorMessage = `
            <i class="fas fa-ban"></i>
            <strong>アクセス拒否:</strong> この操作を実行する権限がありません。
          `;
            errorClass = 'alert-danger';
            break;

          case 409: // Conflict
            errorMessage = `
            <i class="fas fa-exclamation-circle"></i>
            <strong>競合エラー:</strong> ${
              errorResponse.message ||
              'データが既に存在しています'
            }
          `;
            errorClass = 'alert-warning';
            break;

          case 429: // Too Many Requests
            errorMessage = `
            <i class="fas fa-hourglass-half"></i>
            <strong>制限エラー:</strong> リクエストが多すぎます。しばらくお待ちください。
          `;
            errorClass = 'alert-warning';
            break;

          default: // 500系などその他のエラー
            errorMessage = `
            <i class="fas fa-server"></i>
            <strong>サーバーエラー:</strong> 一時的な問題が発生しました。
            <br><small>エラーコード: ${status}</small>
          `;
            errorClass = 'alert-danger';
        }
      } catch (parseError) {
        // JSONの解析に失敗した場合のフォールバック
        errorMessage = `
        <i class="fas fa-exclamation-triangle"></i>
        <strong>エラー:</strong> サーバーから予期しないレスポンスを受信しました。
        <br><small>ステータス: ${status}</small>
      `;
        errorClass = 'alert-danger';
      }

      displayServerError(target, errorMessage, errorClass);
    }
  );
}

// バリデーションエラーの処理
function handleValidationErrors(errorResponse) {
  if (errorResponse.validation_errors) {
    let errorHtml =
      '<i class="fas fa-exclamation-triangle"></i><strong>入力エラー:</strong><ul>';

    for (const [field, errors] of Object.entries(
      errorResponse.validation_errors
    )) {
      errors.forEach((error) => {
        errorHtml += `<li>${error}</li>`;
      });
    }

    errorHtml += '</ul>';
    return errorHtml;
  }

  return (
    errorResponse.message || '入力内容に問題があります'
  );
}

// サーバーエラーの表示
function displayServerError(target, message, alertClass) {
  const errorDiv = document.createElement('div');
  errorDiv.className = `alert ${alertClass} mt-3 server-error`;
  errorDiv.innerHTML = `
    ${message}
    <button type="button" class="btn-close" aria-label="閉じる"></button>
  `;

  // 既存のエラーメッセージを削除
  const existingError =
    target.querySelector('.server-error');
  if (existingError) {
    existingError.remove();
  }

  target.appendChild(errorDiv);

  // 閉じるボタンの機能
  errorDiv
    .querySelector('.btn-close')
    .addEventListener('click', function () {
      errorDiv.remove();
    });
}

バリデーションエラーの処理

フォームのリアルタイムバリデーションとエラー表示の実装例をご紹介します。

シナリオ: リアルタイムフォームバリデーション

html<!-- リアルタイムバリデーション付きフォーム -->
<form id="contact-form">
  <div class="mb-3">
    <label for="name" class="form-label"
      >お名前 <span class="text-danger">*</span></label
    >
    <input
      type="text"
      class="form-control"
      id="name"
      name="name"
      hx-post="/api/validate/name"
      hx-trigger="blur changed delay:500ms"
      hx-target="#name-validation"
      hx-include="[name='name']"
      required
    />
    <div
      id="name-validation"
      class="validation-message"
    ></div>
  </div>

  <div class="mb-3">
    <label for="email" class="form-label"
      >メールアドレス
      <span class="text-danger">*</span></label
    >
    <input
      type="email"
      class="form-control"
      id="email"
      name="email"
      hx-post="/api/validate/email"
      hx-trigger="blur changed delay:500ms"
      hx-target="#email-validation"
      hx-include="[name='email']"
      required
    />
    <div
      id="email-validation"
      class="validation-message"
    ></div>
  </div>

  <button
    type="submit"
    class="btn btn-primary"
    hx-post="/api/contact/send"
    hx-target="#submission-result"
    hx-indicator="#submit-spinner"
  >
    <span
      id="submit-spinner"
      class="htmx-indicator spinner-border spinner-border-sm"
    ></span>
    送信する
  </button>

  <div id="submission-result"></div>
</form>

バリデーションエラー処理の実装

javascript// バリデーションエラー処理の設定
function setupValidationErrorHandling() {
  // バリデーション結果の処理
  document.body.addEventListener(
    'htmx:afterRequest',
    function (event) {
      const target = event.detail.target;

      // バリデーション結果のターゲットかチェック
      if (target.classList.contains('validation-message')) {
        const xhr = event.detail.xhr;
        const inputElement =
          getInputForValidationTarget(target);

        if (xhr.status === 200) {
          handleValidationSuccess(target, inputElement);
        } else if (xhr.status === 422) {
          // Unprocessable Entity
          const errorData = JSON.parse(xhr.responseText);
          handleValidationError(
            target,
            inputElement,
            errorData
          );
        }
      }
    }
  );
}

// 入力要素の取得
function getInputForValidationTarget(target) {
  const targetId = target.id;
  const inputId = targetId.replace('-validation', '');
  return document.getElementById(inputId);
}

// バリデーション成功時の処理
function handleValidationSuccess(target, inputElement) {
  target.innerHTML = `
    <small class="text-success">
      <i class="fas fa-check-circle"></i> 正常に入力されています
    </small>
  `;

  // 入力フィールドのスタイルを成功状態に
  inputElement.classList.remove('is-invalid');
  inputElement.classList.add('is-valid');
}

// バリデーションエラー時の処理
function handleValidationError(
  target,
  inputElement,
  errorData
) {
  const errors = errorData.errors || [errorData.message];

  let errorHtml = '<div class="text-danger small">';
  errors.forEach((error) => {
    errorHtml += `<div><i class="fas fa-times-circle"></i> ${error}</div>`;
  });
  errorHtml += '</div>';

  target.innerHTML = errorHtml;

  // 入力フィールドのスタイルをエラー状態に
  inputElement.classList.remove('is-valid');
  inputElement.classList.add('is-invalid');
}

タイムアウトエラーの処理

長時間かかる処理でのタイムアウト設定とエラー処理の実装例です。

シナリオ: ファイルアップロード時のタイムアウト

html<!-- ファイルアップロードフォーム -->
<form
  id="file-upload-form"
  hx-post="/api/files/upload"
  hx-encoding="multipart/form-data"
  hx-target="#upload-result"
  hx-indicator="#upload-progress"
  hx-ext="timeout"
  hx-timeout="30000"
>
  <div class="mb-3">
    <label for="file" class="form-label"
      >ファイルを選択</label
    >
    <input
      type="file"
      class="form-control"
      id="file"
      name="file"
      required
    />
  </div>

  <button type="submit" class="btn btn-primary">
    アップロード開始
  </button>

  <div id="upload-progress" class="htmx-indicator mt-3">
    <div class="progress">
      <div
        class="progress-bar progress-bar-striped progress-bar-animated"
        role="progressbar"
        style="width: 100%"
      >
        アップロード中...
      </div>
    </div>
  </div>

  <div id="upload-result"></div>
</form>

タイムアウトエラー処理の実装

javascript// タイムアウトエラー処理の設定
function setupTimeoutErrorHandling() {
  // タイムアウトエラーの処理
  document.body.addEventListener(
    'htmx:timeout',
    function (event) {
      const target = event.detail.target;
      const requestConfig = event.detail.requestConfig;

      // プログレスインジケータを停止
      const indicator = document.getElementById(
        'upload-progress'
      );
      if (indicator) {
        indicator.style.display = 'none';
      }

      // タイムアウトエラーメッセージの表示
      const errorDiv = document.createElement('div');
      errorDiv.className = 'alert alert-warning mt-3';
      errorDiv.innerHTML = `
      <div class="d-flex align-items-center">
        <i class="fas fa-clock me-2"></i>
        <div class="flex-grow-1">
          <strong>タイムアウト:</strong> 処理に時間がかかっています。
          <br><small>大きなファイルの場合、処理に時間がかかることがあります。</small>
        </div>
      </div>
      <div class="mt-2">
        <button class="btn btn-sm btn-outline-primary retry-btn me-2">
          <i class="fas fa-redo"></i> 再試行
        </button>
        <button class="btn btn-sm btn-outline-secondary cancel-btn">
          <i class="fas fa-times"></i> キャンセル
        </button>
      </div>
    `;

      // リトライボタンの機能
      const retryButton =
        errorDiv.querySelector('.retry-btn');
      retryButton.addEventListener('click', function () {
        // より長いタイムアウトで再試行
        const form = target.closest('form');
        form.setAttribute('hx-timeout', '60000'); // 60秒に延長

        htmx.trigger(form, 'submit');
        errorDiv.remove();
      });

      // キャンセルボタンの機能
      const cancelButton =
        errorDiv.querySelector('.cancel-btn');
      cancelButton.addEventListener('click', function () {
        errorDiv.remove();
      });

      target.appendChild(errorDiv);
    }
  );

  // プログレス表示の改善
  document.body.addEventListener(
    'htmx:beforeRequest',
    function (event) {
      const target = event.detail.target;

      // ファイルアップロードフォームの場合の特別処理
      if (target.id === 'file-upload-form') {
        setupUploadProgress(target);
      }
    }
  );
}

// アップロードプログレスの設定
function setupUploadProgress(form) {
  const progressDiv = form.querySelector(
    '#upload-progress'
  );
  const progressBar =
    progressDiv.querySelector('.progress-bar');

  let startTime = Date.now();
  let progressInterval = setInterval(function () {
    const elapsed = Date.now() - startTime;
    const timeoutDuration =
      parseInt(form.getAttribute('hx-timeout')) || 30000;
    const progress = Math.min(
      (elapsed / timeoutDuration) * 100,
      95
    ); // 95%で止める

    progressBar.style.width = progress + '%';
    progressBar.textContent = `アップロード中... ${Math.round(
      progress
    )}%`;
  }, 1000);

  // リクエスト完了時にプログレスを停止
  const stopProgress = function () {
    clearInterval(progressInterval);
    progressBar.style.width = '100%';
    progressBar.textContent = '完了';

    setTimeout(() => {
      progressDiv.style.display = 'none';
    }, 1000);
  };

  // 成功・エラー・タイムアウトのいずれでもプログレスを停止
  form.addEventListener('htmx:afterRequest', stopProgress, {
    once: true,
  });
  form.addEventListener(
    'htmx:responseError',
    stopProgress,
    { once: true }
  );
  form.addEventListener('htmx:timeout', stopProgress, {
    once: true,
  });
}

これらの具体例により、HTMX アプリケーションでよく発生するエラーシナリオに対する実践的な対処法をご理解いただけたかと思います。各エラー処理は独立して使用することも、組み合わせて使用することも可能です。

まとめ

本記事では、HTMX アプリケーションにおけるエラーハンドリングとデバッグのコツについて、実践的な手法をご紹介しました。

HTMX の特性を理解し、適切なエラー処理を実装することで、ユーザーエクスペリエンスの向上と開発効率の改善を実現できます。特に重要なポイントは以下の通りです。

エラーハンドリングの要点

  • グローバルなエラーハンドリング設定により、一貫性のある処理を実装する
  • ユーザーにとってわかりやすいエラーメッセージと回復手段を提供する
  • ネットワーク状況やサーバー状態に応じた適切な対応を行う

デバッグの要点

  • ブラウザ開発者ツールと HTMX のログ機能を活用する
  • カスタムデバッグヘルパーにより、開発効率を向上させる
  • 段階的なエラー処理により、問題の特定と解決を迅速化する

実装の要点

  • エラータイプ別(ネットワーク、サーバー、バリデーション、タイムアウト)の適切な処理を実装する
  • リトライ機能やプログレス表示により、ユーザビリティを向上させる
  • エラーイベントの適切なハンドリングにより、堅牢なアプリケーションを構築する

これらの手法を活用することで、より信頼性が高く、ユーザーフレンドリーな HTMX アプリケーションを開発していただけるでしょう。エラーハンドリングは開発の初期段階から考慮することが重要ですので、ぜひプロジェクトの設計時から参考にしていただければと思います。

関連リンク