読み込み中
豆知識
現在の位置づけ: 本番運用中のエラーハンドリング規約(最終更新 2026-07) 最終更新: July 2026
// ❌ BAD: Silent Fallback
try {
const data = await apiClient.get("/documents")
return data
} catch (error) {
console.warn("API failed") // Only logs!
return getCachedData() // User never knows
}
// ✅ GOOD: User-Informed Fallback
try {
const data = await apiClient.get("/documents")
recordEndpointSuccess("/documents")
return { data, isStale: false }
} catch (error) {
recordEndpointFailure("/documents", error.message)
toast.warning("サーバー接続エラー", {
description: "キャッシュデータを表示しています",
})
return { data: getCachedData(), isStale: true }
}
| Status | Read | Write |
|---|---|---|
| Online | ✅ | ✅ |
| Degraded | ✅ (may be stale) | ⚠️ Warn before |
| Offline | ✅ Show cached | ❌ Disabled |
Users should ALWAYS know:
1. Full functionality (backend online)
↓
2. Degraded mode (some endpoints fail)
- Show partial data
- Disable affected features
↓
3. Read-only mode (backend offline, cache available)
- Show cached data with indicator
- Disable all writes
↓
4. Offline screen (no cache)
- Friendly offline message
- Retry button
{
"error": {
"code": "ERROR_CODE",
"message": "Human-readable description",
"details": {},
"correlationId": "c-cb6a1d54",
"timestamp": "2026-01-29T12:34:56+09:00",
"path": "/api/endpoint"
}
}
| Error Type | Status | Description |
|---|---|---|
| Validation | 400 | Client data validation |
| Authentication | 401 | Missing/invalid auth |
| Authorization | 403 | Insufficient permissions |
| Not Found | 404 | Resource doesn't exist |
| Business Rule | 409 | Period locks, over-allocation |
| Rate Limit | 429 | Too many requests |
| Server Error | 500 | Unexpected failures |
| Code | Message | Details |
|---|---|---|
ALLOCATION_EXCEEDS_BANK_AMOUNT |
Allocated exceeds bank transaction | { allocated, limit, remaining } |
ALLOCATION_EXCEEDS_JOURNAL_LINE |
Allocated exceeds journal line | { allocated, limit, journalLineId } |
ALLOCATION_DUPLICATE_PAIR |
Already paired | { bankTxId, journalLineId, existingId } |
| Code | Message | Details |
|---|---|---|
PERIOD_LOCKED |
Cannot write to locked period | { periodId, lockedAt, lockedBy } |
PERIOD_LOCKED_OB |
Opening Balance read-only | { targetPeriod, lockedPeriods } |
| Code | Message | Details |
|---|---|---|
NON_INTEGER_JPY_AMOUNT |
Non-integer JPY detected | { field, value } |
TRIAL_BALANCE_IMBALANCE |
TB doesn't balance | { debits, credits, difference } |
| Code | Message | Details |
|---|---|---|
LINE_TAX_RATE_MISMATCH |
Tax rate mismatch | { lineRate, effectiveRate, taxCode } |
EVIDENCE_IMMUTABLE |
Cannot delete immutable evidence | { documentId, preservationPeriod } |
| Code | Message | Details |
|---|---|---|
INSUFFICIENT_PERMISSIONS |
User lacks permissions | { required, userRole, resource } |
ユーザー向け API 失敗は必ず 原因 + 次の一手 + 相関 ID を見せ、詰まったら 問題を報告(Mushi) へ繋げます。
import { handleMutationError } from "@/lib/mutation-error-handler"
useMutation({
mutationFn: saveSomething,
// 操作名のみ渡す(「〜に失敗しました」はヘルパーが付ける)
onError: handleMutationError("保存"),
})
ERR_* → トーストに「設定を開く」CTAreportErrorToMushi(error_code / request_id をタグ付き)showAiFailureToast / handleAiStreamFailure(contact-support も Mushi)toast.error(error.message) — ESLint local/no-raw-error-message-toastRFC 9457 の code / detail / guidance / requestId / retryable はバックエンドのカタログが SSOT です(scripts/sync-error-catalog.mjs)。
import { useBackendStatus } from '@/hooks/use-backend-status'
function MyComponent() {
const { status, canWrite, refresh } = useBackendStatus()
if (status === 'offline') {
return <OfflineScreen featureName="書類作成" onRetry={refresh} />
}
return (
<Button disabled={!canWrite} onClick={handleSave}>
{canWrite ? '保存' : '接続エラー'}
</Button>
)
}
import { withStatusTracking, withErrorToast } from "@/hooks/use-backend-status"
// READ operations - track but don't toast
const result = await withStatusTracking(
() => apiClient.get("/documents"),
"/documents"
)
// WRITE operations - toast on failure
const result = await withErrorToast(
() => apiClient.delete(`/documents/${id}`),
"ドキュメントの削除に失敗しました",
{ showOfflineWarning: true }
)
// features/documents/apis.ts
export const documentsApi = {
async getAll() {
return withStatusTracking(() => apiClient.get("/documents"), "/documents")
},
async create(data: CreateDocumentDto) {
return withErrorToast(
() => apiClient.post("/documents", data),
"ドキュメント作成に失敗しました",
{ showOfflineWarning: true }
)
},
}
export function useDocuments() {
const { status } = useBackendStatus()
return useQuery({
queryKey: ["documents"],
queryFn: async () => {
try {
const result = await apiClient.get("/documents")
recordEndpointSuccess("/documents")
return result.data
} catch (error) {
recordEndpointFailure("/documents", error.message)
throw error
}
},
staleTime: status === "offline" ? Infinity : 5 * 60 * 1000,
retry: status === "offline" ? false : 3,
})
}
| Component | Usage |
|---|---|
BackendStatusBanner |
Full-width status banner |
BackendStatusBadge |
Small inline indicator |
OfflineScreen |
Full page offline state |
ReadOnlyModeIndicator |
Shows when writes disabled |
StaleDataIndicator |
Shows when viewing cached data |
// In main layout
<BackendStatusBanner hideWhenOnline={true} />
// Next to write buttons
<ReadOnlyModeIndicator />
// When showing potentially stale data
<StaleDataIndicator lastFetched={lastFetchedAt} />
エラーハンドリングの実装チェックリスト(本番運用中)。
| Priority | Files | Good | Fixed | Remaining |
|---|---|---|---|---|
| P1 Critical | 12 | 10 | 2 | 0 |
| P2 High | 18 | 18 | 0 | 0 |
| P3 Medium | 25 | 15 | 10 | 0 |
| P4 Low | 11 | 9 | 2 | 0 |
| Total | 75 | 59 | 16 | 0 |
recordEndpointSuccess/recordEndpointFailurerecordEndpointSuccess/recordEndpointFailuretoast.warning when using cached dataif (import.meta.env.PROD) throw errormeta: { suppressErrorToast: true } to opt out| File | Purpose |
|---|---|
src/hooks/use-backend-status.ts |
Centralized hook |
src/components/ui/backend-status-indicator.tsx |
UI components |
src/lib/query-config.ts |
QueryCache error handlers |
Maintained by: Engineering Team
読み込み中
豆知識