はじめに
前回の記事ではJavaScriptのDOM操作として要素の取得・作成・追加・削除からテンプレートリテラルを使った動的なHTML生成まで解説しました。今回はJavaScriptのエラーハンドリングを解説します。「エラーが発生してページが壊れた」「どこで何が起きているかわからない」「APIの通信失敗をユーザーにうまく伝えられない」という悩みをエラーハンドリングで解決できます。エラーを適切に処理することで、予期しない問題が起きてもアプリケーションが安全に動き続けるようになります。
1. エラーとは何か
1.1 JavaScriptのエラーの種類
JavaScriptのエラーは大きく2種類:
① 構文エラー(SyntaxError):
→ コードの書き方が間違っている
→ ブラウザがコードを実行する前に検出される
→ 例:括弧や波カッコの閉じ忘れ
② 実行時エラー(Runtime Error):
→ コードの実行中に発生する
→ try/catch で捕捉できる
よく遭遇するエラーの種類:
ReferenceError:
→ 存在しない変数を参照した
→ 例:console.log( undeclaredVariable )
TypeError:
→ 値の型が間違っている
→ 例:null.textContent = 'hello'
→ 例:undefinedValue()
RangeError:
→ 値が許容範囲を超えている
→ 例:new Array( -1 )
URIError:
→ URIの処理で問題が発生した
→ 例:decodeURIComponent( '%' )
NetworkError(fetchなど):
→ ネットワーク通信に失敗した
→ 例:存在しないURLへのfetch
SyntaxError(JSON.parse):
→ 不正なJSONをパースした
→ 例:JSON.parse( 'invalid json' )
2. try・catch・finally の基本
2.1 try・catch の基本構文
/*
try:
→ エラーが発生するかもしれないコードを書く
catch:
→ エラーが発生したときの処理を書く
→ エラーオブジェクトが引数として渡される
finally:
→ エラーの有無に関わらず必ず実行される
→ クリーンアップ処理に使う
*/
try {
// エラーが発生するかもしれない処理
const data = JSON.parse( '{ invalid json }' );
console.log( data );
} catch ( error ) {
// エラーが発生したときの処理
console.error( 'エラーが発生しました:', error.message );
} finally {
// 必ず実行される処理
console.log( '処理が完了しました' );
}
2.2 エラーオブジェクトのプロパティ
try {
null.textContent = 'hello'; // TypeError が発生
} catch ( error ) {
console.log( error.name ); // 'TypeError'
console.log( error.message ); // "Cannot set properties of null"
console.log( error.stack ); // スタックトレース(どこで発生したか)
// エラーの種類によって処理を分ける
if ( error instanceof TypeError ) {
console.log( 'TypeError が発生しました' );
} else if ( error instanceof ReferenceError ) {
console.log( 'ReferenceError が発生しました' );
} else {
console.log( '予期しないエラー:', error );
}
}
2.3 throw でエラーを発生させる
/*
throw:
意図的にエラーを発生させる。
バリデーションや前提条件のチェックに使う。
*/
function divide( a, b ) {
if ( typeof a !== 'number' || typeof b !== 'number' ) {
throw new TypeError( '引数は数値である必要があります' );
}
if ( b === 0 ) {
throw new RangeError( '0で割ることはできません' );
}
return a / b;
}
try {
console.log( divide( 10, 2 ) ); // 5
console.log( divide( 10, 0 ) ); // RangeError が発生
} catch ( error ) {
if ( error instanceof RangeError ) {
console.error( '計算エラー:', error.message );
// → 計算エラー: 0で割ることはできません
} else {
console.error( '予期しないエラー:', error );
}
}
2.4 finally の使い道
// ローディング状態の管理(finallyで確実に解除する)
function showLoading() {
document.querySelector( '.loading' ).classList.add( 'is-visible' );
}
function hideLoading() {
document.querySelector( '.loading' ).classList.remove( 'is-visible' );
}
async function fetchData( url ) {
showLoading();
try {
const response = await fetch( url );
const data = await response.json();
return data;
} catch ( error ) {
console.error( 'データの取得に失敗:', error );
throw error; // 呼び出し元に再スローする
} finally {
hideLoading(); // 成功・失敗に関わらず必ず実行
// → try内でreturnしても・catch内でthrowしても実行される
}
}
3. カスタムエラーを作る
3.1 Errorクラスを継承する
/*
カスタムエラー:
Errorクラスを継承して独自のエラー型を作る。
メリット:
→ instanceof で種類を判別できる
→ エラーに追加情報を持たせられる
→ エラーの意味が明確になる
*/
// HTTPエラー
class HttpError extends Error {
constructor( status, message ) {
super( message );
this.name = 'HttpError';
this.status = status;
}
}
// バリデーションエラー
class ValidationError extends Error {
constructor( field, message ) {
super( message );
this.name = 'ValidationError';
this.field = field;
}
}
// ネットワークエラー
class NetworkError extends Error {
constructor( message = 'ネットワークに接続できません' ) {
super( message );
this.name = 'NetworkError';
}
}
// 使用例
async function fetchUser( id ) {
if ( !id ) {
throw new ValidationError( 'id', 'ユーザーIDは必須です' );
}
let response;
try {
response = await fetch( `/api/users/${ id }` );
} catch {
throw new NetworkError();
}
if ( !response.ok ) {
throw new HttpError(
response.status,
`ユーザーの取得に失敗しました(${ response.status })`
);
}
return response.json();
}
// 呼び出し側でエラーを種類別に処理する
async function loadUser( id ) {
try {
const user = await fetchUser( id );
renderUser( user );
} catch ( error ) {
if ( error instanceof ValidationError ) {
showFieldError( error.field, error.message );
} else if ( error instanceof HttpError ) {
if ( error.status === 404 ) {
showMessage( 'ユーザーが見つかりませんでした' );
} else if ( error.status === 401 ) {
redirectToLogin();
} else {
showMessage( 'サーバーエラーが発生しました' );
}
} else if ( error instanceof NetworkError ) {
showMessage( 'インターネット接続を確認してください' );
} else {
console.error( '予期しないエラー:', error );
showMessage( '予期しないエラーが発生しました' );
}
}
}
4. Promiseのエラーハンドリング
4.1 async/await と try/catch
// async/await では try/catch でエラーを捕捉する
async function getPost( id ) {
try {
const response = await fetch( `/api/posts/${ id }` );
// fetchはネットワークエラー以外でthrowしないため
// レスポンスのステータスコードを必ず確認する
if ( !response.ok ) {
throw new HttpError(
response.status,
`HTTPエラー: ${ response.status }`
);
}
const post = await response.json();
return post;
} catch ( error ) {
// エラーをそのまま再スローして呼び出し元で処理する
throw error;
}
}
// ============================================
// 複数のAPIを並列で取得する場合
async function getDashboardData() {
try {
// Promise.all:すべて成功したら結果を返す
// 1つでも失敗したらcatchに入る
const [ user, posts, notifications ] = await Promise.all( [
fetch( '/api/user' ).then( ( r ) => r.json() ),
fetch( '/api/posts' ).then( ( r ) => r.json() ),
fetch( '/api/notifications' ).then( ( r ) => r.json() ),
] );
return { user, posts, notifications };
} catch ( error ) {
console.error( 'ダッシュボードデータの取得に失敗:', error );
throw error;
}
}
// ============================================
// Promise.allSettled:すべての結果を待って
// 成功・失敗それぞれ処理する
async function getDataWithFallback() {
const results = await Promise.allSettled( [
fetch( '/api/main-data' ).then( ( r ) => r.json() ),
fetch( '/api/optional-data' ).then( ( r ) => r.json() ),
] );
const [ mainResult, optionalResult ] = results;
if ( mainResult.status === 'rejected' ) {
throw new Error( 'メインデータの取得に失敗しました' );
}
return {
main: mainResult.value,
optional: optionalResult.status === 'fulfilled'
? optionalResult.value
: null, // オプションデータは失敗してもnullで続行
};
}
4.2 グローバルなエラーハンドリング
/*
キャッチされなかったエラーを
グローバルで処理する仕組み。
開発中のデバッグ・本番でのエラーロギングに使う。
*/
// 同期処理のキャッチされなかったエラー
window.addEventListener( 'error', ( event ) => {
console.error( 'キャッチされなかったエラー:', {
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
error: event.error,
} );
// エラーログをサーバーに送信する
sendErrorLog( {
type: 'uncaught_error',
message: event.message,
stack: event.error?.stack,
} );
} );
// Promiseのキャッチされなかったエラー
window.addEventListener( 'unhandledrejection', ( event ) => {
console.error( 'キャッチされなかったPromiseエラー:', event.reason );
// エラーログをサーバーに送信する
sendErrorLog( {
type: 'unhandled_rejection',
reason: String( event.reason ),
} );
// デフォルトの動作(コンソールへの警告)を抑制する
event.preventDefault();
} );
// エラーログを送信するユーティリティ
async function sendErrorLog( data ) {
try {
await fetch( '/api/error-log', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify( {
...data,
url: window.location.href,
userAgent: navigator.userAgent,
timestamp: new Date().toISOString(),
} ),
} );
} catch {
// ログ送信自体が失敗してもアプリを止めない
}
}
5. ユーザーへのエラー表示パターン
5.1 トースト通知
class Toast {
#container;
#defaultDuration;
constructor( duration = 4000 ) {
this.#defaultDuration = duration;
this.#createContainer();
}
#createContainer() {
this.#container = document.createElement( 'div' );
this.#container.className = 'toast-container';
this.#container.setAttribute( 'aria-live', 'polite' ); // スクリーンリーダー対応
document.body.appendChild( this.#container );
}
#show( message, type, duration ) {
const toast = document.createElement( 'div' );
toast.className = `toast toast--${ type }`;
toast.textContent = message;
toast.setAttribute( 'role', 'alert' );
this.#container.appendChild( toast );
// アニメーション後に削除する
setTimeout( () => {
toast.classList.add( 'toast--hiding' );
toast.addEventListener( 'animationend', () => toast.remove() );
}, duration ?? this.#defaultDuration );
return toast;
}
success( message, duration ) {
return this.#show( message, 'success', duration );
}
error( message, duration ) {
return this.#show( message, 'error', duration ?? 6000 );
}
warning( message, duration ) {
return this.#show( message, 'warning', duration );
}
info( message, duration ) {
return this.#show( message, 'info', duration );
}
}
// シングルトンとして使う
const toast = new Toast();
// 使用例
toast.success( 'フォームを送信しました!' );
toast.error( 'エラーが発生しました。もう一度お試しください。' );
toast.warning( '入力内容に問題があります' );
toast.info( '自動保存しました' );
5.2 フォームのエラー表示
class FormErrorHandler {
constructor( formSelector ) {
this.form = document.querySelector( formSelector );
}
// フィールドのエラーを表示する
showFieldError( fieldName, message ) {
const field = this.form.querySelector( `[name="${ fieldName }"]` );
const errorEl = this.form.querySelector( `#${ fieldName }-error` );
if ( field ) {
field.classList.add( 'is-error' );
field.setAttribute( 'aria-invalid', 'true' );
field.setAttribute( 'aria-describedby', `${ fieldName }-error` );
}
if ( errorEl ) {
errorEl.textContent = message;
errorEl.removeAttribute( 'hidden' );
}
}
// フィールドのエラーをクリアする
clearFieldError( fieldName ) {
const field = this.form.querySelector( `[name="${ fieldName }"]` );
const errorEl = this.form.querySelector( `#${ fieldName }-error` );
if ( field ) {
field.classList.remove( 'is-error' );
field.removeAttribute( 'aria-invalid' );
}
if ( errorEl ) {
errorEl.textContent = '';
errorEl.setAttribute( 'hidden', '' );
}
}
// フォーム全体のエラーを表示する
showFormError( message ) {
let alertEl = this.form.querySelector( '.form-alert' );
if ( !alertEl ) {
alertEl = document.createElement( 'div' );
alertEl.className = 'form-alert form-alert--error';
alertEl.setAttribute( 'role', 'alert' );
this.form.prepend( alertEl );
}
alertEl.textContent = message;
alertEl.removeAttribute( 'hidden' );
// フォームアラートにフォーカスを移す
alertEl.setAttribute( 'tabindex', '-1' );
alertEl.focus();
}
// すべてのエラーをクリアする
clearAll() {
this.form.querySelectorAll( '.is-error' ).forEach( ( el ) => {
el.classList.remove( 'is-error' );
el.removeAttribute( 'aria-invalid' );
} );
this.form.querySelectorAll( '[id$="-error"]' ).forEach( ( el ) => {
el.textContent = '';
el.setAttribute( 'hidden', '' );
} );
this.form.querySelector( '.form-alert' )?.setAttribute( 'hidden', '' );
}
}
5.3 エラー境界(エラー状態のUI)
class ErrorBoundary {
constructor( containerSelector ) {
this.container = document.querySelector( containerSelector );
this.#saveOriginalContent();
}
#originalContent = '';
#saveOriginalContent() {
this.#originalContent = this.container.innerHTML;
}
// エラー表示に切り替える
showError( {
title = 'エラーが発生しました',
message = '予期しないエラーが発生しました。',
retry = null,
} = {} ) {
this.container.innerHTML = `
<div class="error-state" role="alert">
<div class="error-icon" aria-hidden="true">⚠️</div>
<h2 class="error-title">${ title }</h2>
<p class="error-message">${ message }</p>
${ retry ? `
<button class="btn btn-retry">
もう一度試す
</button>
` : '' }
</div>
`;
if ( retry ) {
this.container.querySelector( '.btn-retry' )
?.addEventListener( 'click', retry );
}
}
// 空の状態を表示する
showEmpty( message = 'データが見つかりませんでした' ) {
this.container.innerHTML = `
<div class="empty-state">
<div class="empty-icon" aria-hidden="true">📭</div>
<p class="empty-message">${ message }</p>
</div>
`;
}
// ローディング表示に切り替える
showLoading( message = '読み込み中...' ) {
this.container.innerHTML = `
<div class="loading-state" aria-busy="true" aria-label="${ message }">
<div class="spinner" aria-hidden="true"></div>
<p>${ message }</p>
</div>
`;
}
// 元のコンテンツに戻す
restore() {
this.container.innerHTML = this.#originalContent;
}
}
// 実践的な使用例
class PostLoader {
#boundary;
constructor( containerSelector ) {
this.#boundary = new ErrorBoundary( containerSelector );
}
async load( postId ) {
this.#boundary.showLoading( '記事を読み込んでいます...' );
try {
const response = await fetch( `/api/posts/${ postId }` );
if ( !response.ok ) {
if ( response.status === 404 ) {
this.#boundary.showEmpty( '記事が見つかりませんでした' );
return;
}
throw new HttpError( response.status, 'サーバーエラー' );
}
const post = await response.json();
// データがない場合
if ( !post ) {
this.#boundary.showEmpty();
return;
}
this.#renderPost( post );
} catch ( error ) {
this.#boundary.showError( {
title: '記事の読み込みに失敗しました',
message: error instanceof NetworkError
? 'インターネット接続を確認してください'
: 'しばらくしてからもう一度お試しください',
retry: () => this.load( postId ),
} );
}
}
#renderPost( post ) {
// 記事を描画する処理
}
}
6. エラーハンドリングのベストプラクティス
6.1 エラーを握りつぶさない
// ❌ エラーを握りつぶすのはNG
try {
riskyOperation();
} catch ( error ) {
// 何もしない → エラーが発生したことがわからなくなる
}
// ❌ console.logだけでは不十分
try {
riskyOperation();
} catch ( error ) {
console.log( error ); // ログだけ残してそのまま続行
}
// ✅ エラーを適切に処理する
try {
riskyOperation();
} catch ( error ) {
// ① ログに記録する
console.error( '操作が失敗しました:', error );
// ② ユーザーに通知する
toast.error( 'エラーが発生しました。もう一度お試しください。' );
// ③ 必要なら再スローする
throw error;
// ④ またはフォールバックの処理をする
return defaultValue;
}
6.2 エラーメッセージを使い分ける
/*
ユーザーへのメッセージと
開発者向けのログは分ける。
ユーザーには:
→ わかりやすい日本語
→ 次にどうすればよいかを伝える
→ 技術的な詳細は不要
開発者には:
→ エラーの種類・スタックトレース
→ どのAPIが失敗したか
→ リクエストのパラメータなど
*/
async function submitForm( data ) {
try {
const response = await fetch( '/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify( data ),
} );
if ( !response.ok ) {
const errorData = await response.json().catch( () => ({}) );
// 開発者向けログ(詳細)
console.error( 'APIエラー:', {
status: response.status,
url: response.url,
data: errorData,
payload: data,
} );
// ユーザー向けメッセージ(シンプル)
const userMessage = response.status === 422
? '入力内容に問題があります。確認してもう一度お試しください。'
: 'エラーが発生しました。しばらくしてからもう一度お試しください。';
throw new Error( userMessage );
}
toast.success( 'お問い合わせを送信しました!' );
} catch ( error ) {
toast.error( error.message );
}
}
6.3 リトライ処理
// 一時的なエラーに対してリトライする
async function fetchWithRetry( url, options = {}, maxRetries = 3 ) {
let lastError;
for ( let attempt = 1; attempt <= maxRetries; attempt++ ) {
try {
const response = await fetch( url, options );
if ( !response.ok ) {
// 5xx エラーはリトライする・4xx はリトライしない
if ( response.status >= 500 ) {
throw new HttpError( response.status, 'サーバーエラー' );
}
return response; // 4xx はそのまま返す
}
return response;
} catch ( error ) {
lastError = error;
console.warn( `試行 ${ attempt }/${ maxRetries } 失敗:`, error.message );
// 最後の試行ならリトライしない
if ( attempt === maxRetries ) break;
// 指数バックオフ:待ち時間を指数的に増やす
const waitMs = Math.min( 1000 * 2 ** ( attempt - 1 ), 10000 );
// → 1秒 → 2秒 → 4秒 → 最大10秒
await new Promise( ( resolve ) => setTimeout( resolve, waitMs ) );
}
}
throw lastError;
}
// 使用例
try {
const response = await fetchWithRetry( '/api/data', {}, 3 );
const data = await response.json();
renderData( data );
} catch ( error ) {
console.error( '3回リトライしましたが失敗しました:', error );
showError( 'データの取得に失敗しました' );
}
7. 実践チェックリスト
✓ fetch のレスポンスで response.ok を確認して HTTPエラーを処理しているか
✓ async/await を使うときに try/catch でエラーを捕捉しているか
✓ finally を使ってローディング状態のクリーンアップを確実に行っているか
✓ カスタムエラークラスで instanceof によるエラー種別の判別ができているか
✓ ユーザーへのエラーメッセージと開発者向けログを分けて管理しているか
✓ window.addEventListener('unhandledrejection') でキャッチされなかったPromiseエラーを補足しているか
✓ エラーを握りつぶさず・ログに記録してユーザーに通知しているか
✓ ネットワークエラーなど一時的な障害に対してリトライ処理を実装しているか
まとめ
今回はJavaScriptのエラーハンドリングをtry・catch・finallyの基本からカスタムエラー・Promiseのエラー処理・ユーザーへの通知パターンまで解説しました。ポイントをまとめると:
try/catchでエラーを捕捉し・error.name/error.message/error.stackでエラーの内容を把握するfinallyはエラーの有無に関わらず必ず実行されるのでローディング状態の解除など確実に行いたい処理に使うthrowで意図的にエラーを発生させてバリデーションや前提条件のチェックをするErrorを継承したカスタムエラークラスを作るとinstanceofで種類を判別してエラーごとに適切な処理ができるfetchはネットワークエラー以外は throw しないためresponse.okを必ず確認してHTTPエラーを自分で処理するPromise.allSettledを使うとすべての結果を待ちながら成功・失敗それぞれを個別に処理できるwindow.addEventListener('unhandledrejection')でキャッチされなかったPromiseエラーをグローバルで処理できる- エラーを握りつぶさず・ユーザーには平易なメッセージを・開発者向けには詳細なログを残す習慣をつける
次の記事では、JavaScriptのモジュールシステムを解説します。importとexportの使い方でコードをファイルに分割して整理する方法をお届けします。お楽しみに!
コメント