
SvelteKit Hooks実践:handleError・handleFetch・hooks.client.tsでエラー処理とfetch制御を学ぶ
SvelteKit Hooksの実践編として、handleErrorによるエラー処理、handleFetchによるサーバーサイドfetchの制御、hooks.client.tsでのクライアントエラー対応を解説します。
シリーズ:SvelteKit Hooks 完全ガイド
01
Hooks の基本と3ファイルの違い
02
handle で認証を一元化する
03
handleError・handleFetch・クライアント
第2回でhandle 関数による認証の一元化が完成しました。今回はシリーズBの最終回として、残りの Hooks フック関数を実装してアプリを完成形に仕上げます。
具体的には下記の流れでトースト通知を行います。
①handleError (server) でサーバーの未捕捉エラーをログに記録
②handleFetch でサーバーサイドの fetch をインターセプト
③hooks.client.ts のhandleError でブラウザエラーをトースト通知に変換
最後にこれまでのシリーズ を通じた全体像をおさらいします。
💡 前提条件
第1回・第2回で作成したsrc/hooks.server.ts ・src/hooks.ts ・src/lib/server/session.ts が存在する状態で進めます。
handleError(server)― 未捕捉エラーをログに記録する
🖥 hooks.server.ts
handleError はload 関数やサーバー処理の中で 予期しないエラーが発生したとき に呼ばれます。try/catch で意図的に処理したエラーではなく、 捕捉できなかったエラー が対象です。
シリーズAの第3回で実装した+error.svelte はエラーを「表示する」側の仕組みでした。handleError はその一段手前で「エラーを記録・加工する」役割を担います。
hooks.server.ts の handleError
- サーバーで発生した未捕捉エラーを受け取る
- エラーをログに記録する(console / 外部サービス)
- ブラウザに返すエラーメッセージをカスタマイズできる
- スタックトレースなど機密情報を隠せる
hooks.client.ts の handleError
- ブラウザで発生した未捕捉エラーを受け取る
- トースト通知などUIフィードバックを出せる
- 外部エラー監視サービスに送信できる
- +error.svelte に表示するメッセージを整形できる
handleError の引数
handleError はerror ・event ・status ・message の4つを受け取ります。
import type { Handle, HandleServerError } from '@sveltejs/kit';
import { redirect } from '@sveltejs/kit';
import { getSession } from '$lib/server/session';
/* ── handle(第2回と同じ) ── */
const PROTECTED_ROUTES = ['/mypage'];
export const handle: Handle = async ({ event, resolve }) => {
const sessionId = event.cookies.get('session_id');
event.locals.user = sessionId ? getSession(sessionId) : null;
const isProtected = PROTECTED_ROUTES.some((r) => event.url.pathname.startsWith(r));
if (isProtected && !event.locals.user) {
redirect(303, `/login?redirectTo=${event.url.pathname}`);
}
const response = await resolve(event);
response.headers.set('X-Frame-Options', 'SAMEORIGIN');
return response;
};
/* ── handleError(今回追加) ── */
export const handleError: HandleServerError = ({ error, event, status, message }) => {
// 404 はログ不要(正常な「ページが見つからない」アクセス)
if (status !== 404) {
// サーバー側でスタックトレース付きのログを記録
console.error(`[handleError] ${status} ${event.url.pathname}`, error);
}
// ブラウザに返すエラーオブジェクト
// message はそのまま返す(error() で指定したメッセージ)
// スタックトレースや内部情報は絶対に返さない
return {
message: status === 404 ? 'ページが見つかりません' : message,
};
};❌ Sentry などへの連携はこう書く
エラー監視サービスに送りたい場合は、handleError の中でSDKを呼び出します。Sentry.captureException(error) などをconsole.error の代わりに置くだけで連携できます。詳細な設定は各サービスの公式ドキュメントを参照してください。
⚠️ ️ スタックトレースをブラウザに返してはいけない
handleError の戻り値はそのまま$page.error に入り、+error.svelte でアクセスできます。error.stack など内部情報を含めると、攻撃者にアプリの構造を教えることになります。ユーザー向けには 簡潔なメッセージのみ を返してください。
handleFetch ― サーバーサイド fetch をインターセプトする
🖥 hooks.server.ts
handleFetch はload 関数の中でfetch() を呼んだとき、そのリクエストを実行前にインターセプトします。主な用途は 内部APIのURLを書き換えること です。
なぜ URL の書き換えが必要か
ブラウザからの fetch
fetch('/api/products')
→ ブラウザから
http://localhost:5173/api/products
を呼ぶ(問題なし)
サーバー(load)からの fetch
fetch('/api/products')
→ サーバーから自分自身の
http://localhost:5173/api/products
を呼ぶ(無駄なHTTPラウンドトリップ)
+page.server.ts のload 内でfetch('/api/products') を呼ぶと、サーバーから自分自身にHTTPリクエストを投げることになります。handleFetch でこのリクエストを捕まえて内部処理に切り替えることができます。
import type { Handle, HandleServerError, HandleFetch } from '@sveltejs/kit';
// ... handle と handleError は省略 ...
export const handleFetch: HandleFetch = ({ request, fetch, event }) => {
const url = new URL(request.url);
// 自サイトの /api/* へのリクエストをインターセプト
if (url.origin === event.url.origin && url.pathname.startsWith('/api/')) {
// 認証ヘッダーを自動付与(サーバー間通信のため Cookie は自動では届かない)
const sessionId = event.cookies.get('session_id') ?? '';
return fetch(new Request(request, {
headers: {
...Object.fromEntries(request.headers),
'x-session-id': sessionId,
},
}));
}
// その他のリクエストはそのまま通す
return fetch(request);
};📌 handleFetch のよくある使い方まとめ
① 内部APIへのCookie・認証ヘッダーを自動付与する(今回の実装)。
② ステージング環境で外部APIのURLを差し替える(dev フラグと組み合わせる)。
③ すべての外部リクエストにリトライ処理を追加する。
hooks.client.ts ― ブラウザエラーをトースト通知に変換する
🌐 hooks.client.ts
hooks.client.ts のhandleError はブラウザ上で発生した未捕捉エラーを受け取ります。ここではエラーが起きたときにトースト通知を表示する実装を行います。
トーストストアを作る
まずトースト通知の状態を管理する Svelte ストアを作ります。
import { writable } from 'svelte/store';
export type Toast = { id: string; message: string; type: 'error' | 'info' };
export const toasts = writable<Toast[]>([]);
export function addToast(message: string, type: Toast['type'] = 'error') {
const id = Math.random().toString(36).slice(2);
toasts.update((t) => [...t, { id, message, type }]);
// 4秒後に自動で消す
setTimeout(() => toasts.update((t) => t.filter((x) => x.id !== id)), 4000);
} hooks.client.ts で handleError を実装する
import type { HandleClientError } from '@sveltejs/kit';
import { addToast } from '$lib/toast';
export const handleError: HandleClientError = ({ error, status, message }) => {
// 404 はトースト不要(ページが存在しないだけ)
if (status !== 404) {
// エラーメッセージをトーストで通知
addToast(message ?? '予期しないエラーが発生しました', 'error');
console.error('[client error]', error);
}
// +error.svelte の $page.error.message に入る値を返す
return { message: message ?? '予期しないエラーが発生しました' };
}; トーストコンポーネントをルートレイアウトに配置する
⚠ エラーが発生しました
予期しないエラーが発生しました
ブラウザで未捕捉エラーが発生すると、画面右下などにこのようなトーストが4秒間表示されます。
<script lang="ts">
import { toasts } from '$lib/toast';
</script>
<div class="toast-container">
{#each $toasts as toast (toast.id)}
<div class="toast toast-{toast.type}">
{toast.message}
</div>
{/each}
</div>
<style>
.toast-container { position: fixed; bottom: 1.5rem; right: 1.5rem; display: flex; flex-direction: column; gap: 8px; z-index: 9999; }
.toast { padding: 12px 16px; border-radius: 8px; font-size: 14px; font-weight: 500; max-width: 320px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); }
.toast-error { background: #a32d2d; color: #fff; border-left: 4px solid #f47d7d; }
.toast-info { background: #185fa5; color: #fff; border-left: 4px solid #7eb8f7; }
</style><script lang="ts">
import ToastContainer from '$lib/components/ToastContainer.svelte';
</script>
<header>
<nav>
<a href="/">SvelteShop</a>
<a href="/products">商品一覧</a>
<a href="/mypage">マイページ</a>
</nav>
</header>
<main>
<slot />
</main>
<!-- 全ページでトーストを表示できるようにルートレイアウトに配置 -->
<ToastContainer />🎯 確認
ブラウザのコンソールでthrow new Error('テスト') を実行すると、画面右下にトーストが4秒間表示されます。またhttp://localhost:5173/products/99999 (存在しないID)にアクセスすると+error.svelte が表示され、サーバーログに[handleError] が記録されることも確認できます。
第3回の完成コード一覧
src/
├─hooks.server.tsUPDATE← handleError・handleFetch 追加
├─hooks.client.tsNEW
└─lib/
├─toast.tsNEW
└─components/
└─ToastContainer.svelteNEW📄
src/hooks.server.ts(完成形)
handle・handleError・handleFetch をすべて含む最終版
import type { Handle, HandleServerError, HandleFetch } from '@sveltejs/kit';
import { redirect } from '@sveltejs/kit';
import { getSession } from '$lib/server/session';
const PROTECTED_ROUTES = ['/mypage'];
export const handle: Handle = async ({ event, resolve }) => {
const sessionId = event.cookies.get('session_id');
event.locals.user = sessionId ? getSession(sessionId) : null;
const isProtected = PROTECTED_ROUTES.some((r) => event.url.pathname.startsWith(r));
if (isProtected && !event.locals.user) {
redirect(303, `/login?redirectTo=${event.url.pathname}`);
}
const response = await resolve(event);
response.headers.set('X-Frame-Options', 'SAMEORIGIN');
return response;
};
export const handleError: HandleServerError = ({ error, event, status, message }) => {
if (status !== 404) {
console.error(`[handleError] ${status} ${event.url.pathname}`, error);
}
return {
message: status === 404 ? 'ページが見つかりません' : message,
};
};
export const handleFetch: HandleFetch = ({ request, fetch, event }) => {
const url = new URL(request.url);
if (url.origin === event.url.origin && url.pathname.startsWith('/api/')) {
const sessionId = event.cookies.get('session_id') ?? '';
return fetch(new Request(request, {
headers: { ...Object.fromEntries(request.headers), 'x-session-id': sessionId },
}));
}
return fetch(request);
};📄
src/hooks.client.ts
ブラウザエラーをトーストで通知
import type { HandleClientError } from '@sveltejs/kit';
import { addToast } from '$lib/toast';
export const handleError: HandleClientError = ({ error, status, message }) => {
if (status !== 404) {
addToast(message ?? '予期しないエラーが発生しました', 'error');
console.error('[client error]', error);
}
return { message: message ?? '予期しないエラーが発生しました' };
};📄
src/lib/toast.ts
トースト管理ストア
import { writable } from 'svelte/store';
export type Toast = { id: string; message: string; type: 'error' | 'info' };
export const toasts = writable<Toast[]>([]);
export function addToast(message: string, type: Toast['type'] = 'error') {
const id = Math.random().toString(36).slice(2);
toasts.update((t) => [...t, { id, message, type }]);
setTimeout(() => toasts.update((t) => t.filter((x) => x.id !== id)), 4000);
} これまでのシリーズを含めた全体のおさらい ― Hooks の全フック一覧
これまでのシリーズを通じて実装してきたすべての Hooks フックをまとめます。
| フック関数 | ファイル | 実行環境 | 主な用途 |
|---|---|---|---|
| handle | hooks.server.ts | サーバー | 全リクエストの前後処理・認証・レスポンスヘッダー |
| handleError | hooks.server.ts | サーバー | サーバー未捕捉エラーのロギング・メッセージ加工 |
| handleFetch | hooks.server.ts | サーバー | load 内 fetch のインターセプト・ヘッダー自動付与 |
| handleError | hooks. client.ts | ブラウザ | ブラウザ未捕捉エラーの通知・外部サービス送信 |
| reroute | hooks.ts | 両方 | URLを変えずに内部ルートを切り替えるエイリアス |
| transport | hooks.ts | 両方 | カスタム型のシリアライズ・デシリアライズ定義 |
第3回・シリーズ全体のまとめ
今回学んだこと
- ✔️
handleError(server)は未捕捉エラーを受け取り、ロギングとブラウザへのメッセージ加工を担う。スタックトレースなど内部情報はブラウザに返してはいけない - ✔️
handleFetchはload内のfetch()をインターセプトする。自サイト内APIへの認証ヘッダー自動付与に使うのが典型パターン - ✔️
hooks.client.tsのhandleErrorはブラウザで発生した未捕捉エラーを受け取る。Svelte ストアと組み合わせてトースト通知を実現できる - ✔️ サーバー・クライアント両面のエラー処理が揃い、
+error.svelte(表示)・handleError(処理)・トースト(通知)の3層でエラーをカバーできた
📌 本シリーズ のまとめ
前シリーズ で学んだ「ページごとのデータフロー」に、本シリーズで「横断的な制御」が加わりました。handle による認証の一元化、handleError によるエラーの完全制御、handleFetch による fetch の透過的な加工、hooks.client.ts によるブラウザ通知 ― これらがすべて実装された商品カタログアプリは、次のシリーズ の認証強化に向けた土台が整っています。
🎉
シリーズ完走、おめでとうございます!
前シリーズ ・本シリーズ を通じて、SvelteKit のルーティング・データ取得・SSR・Hooks による横断処理を体系的に学びました。
商品カタログアプリに認証・エラー処理・トースト通知が加わり、本番に近い形になりました。
✅ handle による認証一元化
✅ event.cookies セッション管理
✅ Form Actions ログインフォーム
✅ handleError サーバーロギング
✅ handleFetch ヘッダー自動付与
✅ hooks.client.ts トースト通知
📘 次のシリーズ
シリーズ:SvelteKit 認証完全ガイド
本シリーズで実装した認証の「足りない部分」を本番レベルに仕上げます。
パスワードハッシュ化(bcrypt)
CSRF対策
セッション有効期限・更新
DB・Redis へのセッション永続化
OAuth・ソーシャルログイン
本番運用チェックリスト