11. エラーハンドリング応用 中級
カスタムErrorクラス
class AppError extends Error {
constructor(message, code) {
super(message);
this.name = this.constructor.name;
this.code = code;
}
}
class PaymentError extends AppError {
constructor(message, code, transactionId) {
super(message, code);
this.transactionId = transactionId;
}
}
class StockError extends AppError {}
// 使い分け
try {
throw new PaymentError('決済失敗', 'PAY_001', 'txn_abc');
} catch (e) {
if (e instanceof PaymentError) {
console.log(`決済エラー [${e.code}]: ${e.message}`);
} else if (e instanceof StockError) {
console.log(`在庫エラー: ${e.message}`);
} else {
throw e; // 未知のエラーは再throw
}
}
リトライ(指数バックオフ)
async function retryFetch(url, options = {}, maxRetries = 3) {
let lastError;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const res = await fetch(url, options);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (err) {
lastError = err;
if (attempt < maxRetries - 1) {
// 待機時間: 1秒 → 2秒 → 4秒...
const delay = 1000 * Math.pow(2, attempt);
console.log(`リトライ ${attempt + 1}/${maxRetries}(${delay}ms後)`);
await new Promise(r => setTimeout(r, delay));
}
}
}
throw new Error(`${maxRetries}回リトライ後も失敗: ${lastError.message}`);
}
// 使い方
const data = await retryFetch('/api/orders');
グローバルエラーハンドリング
// 同期エラーのキャッチ
window.addEventListener('error', (e) => {
console.error('未処理エラー:', e.message, e.filename, e.lineno);
// サーバーにエラーレポートを送信
fetch('/api/error-log', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: e.message,
source: e.filename,
line: e.lineno,
time: new Date().toISOString(),
}),
});
});
// 未処理のPromise rejectionのキャッチ
window.addEventListener('unhandledrejection', (e) => {
console.error('未処理Promise:', e.reason);
e.preventDefault(); // コンソールのデフォルトエラー表示を抑制
});
エラー境界パターン(操作全体をラップ)
async function safePay(orderId, amount) {
try {
// 1. 在庫チェック
await checkStock(orderId);
// 2. 決済実行
const result = await processPayment(orderId, amount);
// 3. 完了通知
showToast('決済完了', 'success');
return result;
} catch (e) {
if (e instanceof StockError) {
showToast(`在庫不足: ${e.message}`, 'error');
} else if (e instanceof PaymentError) {
showToast(`決済失敗: ${e.message}`, 'error');
// 決済ロールバック
await rollbackPayment(e.transactionId);
} else {
showToast('予期しないエラーが発生しました', 'error');
}
console.error('safePay失敗:', e);
return null;
}
}
14. セキュリティとXSS対策 中級
textContent vs innerHTML(XSS防止の基本)
const userInput = '<img src=x onerror=alert("XSS")>';
const el = document.getElementById('output');
// ✅ 安全 — テキストとして表示、HTMLとして解釈されない
el.textContent = userInput;
// ❌ 危険 — HTMLとして解釈され、スクリプトが実行される
el.innerHTML = userInput;
// ✅ 安全にHTMLを構築する方法
const li = document.createElement('li');
li.textContent = userInput; // テキストのみ設定
list.appendChild(li);
HTMLエスケープユーティリティ
const ESCAPE_MAP = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
};
function escapeHTML(str) {
return str.replace(/[&<>"']/g, (ch) => ESCAPE_MAP[ch]);
}
// innerHTML に挿入する必要がある場合に使用
const productName = '<script>悪意あるコード</script>';
row.innerHTML = `<td>${escapeHTML(productName)}</td>`;
安全なJSON解析とeval禁止
// ❌ eval は絶対に使わない(任意コード実行のリスク)
const data = eval('(' + jsonString + ')'); // 危険!
// ✅ JSON.parse を使う
try {
const data = JSON.parse(jsonString);
} catch (e) {
console.error('不正なJSONデータ:', e.message);
}
// ✅ バーコードデータも安全に処理
function parseBarcode(raw) {
const cleaned = raw.replace(/[^0-9]/g, ''); // 数字以外を除去
if (cleaned.length !== 13) return null;
return cleaned;
}
機密データの安全な取り扱い
// 決済後にカード情報をDOMとメモリから消去
function clearSensitiveData() {
// DOM上の値をクリア
const cardInput = document.getElementById('cardNumber');
if (cardInput) {
cardInput.value = '';
cardInput.setAttribute('autocomplete', 'off');
}
// マスキング表示(下4桁のみ)
const display = document.getElementById('cardDisplay');
if (display) display.textContent = '**** **** **** 1234';
}
// カード番号のマスキング関数
function maskCardNumber(num) {
const last4 = num.slice(-4);
return `**** **** **** ${last4}`;
}
// localStorageに機密情報を保存しない
// ❌ localStorage.setItem('cardNumber', '4111111111111111');
// ✅ サーバー側で安全に管理し、トークンのみ保持
localStorage.setItem('paymentToken', 'tok_abc123');