ESLintでコード品質UP!「no-useless-catch」ルールを理解する
"no-useless-catch" とは?
一言で言うと、**「キャッチしたエラーをそのまま再スローするだけの catch
ブロックは無駄である」**という考えに基づいたルールです。
JavaScript では、try...catch...finally
構文を使ってエラーハンドリングを行います。
try {
// エラーが発生する可能性のあるコード
} catch (error) {
// エラーが発生した場合の処理
} finally {
// tryブロック、catchブロックの後に常に実行される処理
}
この catch
ブロックで、単にキャッチした error
オブジェクトを再び throw error;
のように再スローするだけのコードは、実行時の動作に何も影響を与えません。なぜなら、catch
ブロックがなければ、エラーはそのまま上位の呼び出し元に伝播していくからです。
なぜ "useless" (無駄) なのか?
- パフォーマンスへの影響 (微小)
非常に微々たるものですが、余計な処理がない方がパフォーマンスは良くなります。 - 混乱の元
開発者がそのcatch
ブロックに何らかの意図があると誤解し、無駄な調査に時間を費やす可能性があります。 - コードの冗長性
何もしないcatch
ブロックは、コードの行数を増やし、読みやすさを損ねる可能性があります。
具体的な例
悪い例 (no-useless-catch が警告を出す例)
try {
doSomethingThatMightThrow();
} catch (e) {
throw e; // この catch ブロックは無駄
}
この場合、catch
ブロックがなくても、doSomethingThatMightThrow()
で発生したエラーはそのまま外側に伝播するため、上記コードは次のように書き換えても全く同じ動作になります。
doSomethingThatMightThrow(); // エラーはそのまま伝播する
良い例 (no-useless-catch が警告を出さない例)
catch
ブロックに何らかの処理がある場合は、このルールは適用されません。
try {
doSomethingThatMightThrow();
} catch (e) {
console.error("エラーが発生しました:", e); // エラーをログに出力する
throw e; // 処理を行った後に再スロー
}
または、finally
ブロックでクリーンアップなどの処理を行うために try...catch
を使用し、エラー自体は再スローしない場合も問題ありません。
try {
doSomethingThatMightThrow();
} catch (e) {
// エラーを握りつぶす(意図的な場合のみ)
} finally {
cleanUpResources(); // リソースの解放など
}
ESLintの公式ドキュメントでも触れられていますが、finally
ブロックが存在する場合、catch
ブロックが単にエラーを再スローするだけでも有用なケースがあります。例えば、finally
ブロックでリソースの解放などを行う必要がある場合です。
async function trackedFetch(...args) {
actions.inc(); // カウンターを増やす
try {
return await fetch(...args);
} catch (err) {
throw err; // この catch は無駄に見えるが、finally のために必要
} finally {
actions.dec(); // カウンターを減らす
}
}
この例では、Workspace
が失敗した場合でも finally
ブロックで actions.dec()
を実行するために try...catch
が必要になります。もし catch
ブロックがなければ、エラーが発生した時点で finally
ブロックに到達せずに呼び出し元にエラーが伝播してしまう可能性があります。
しかし、この場合でも、catch
ブロック自体は不要であるとESLintは判断します。上記のコードは次のように書き換えることができます。
async function trackedFetch(...args) {
actions.inc();
try {
return await fetch(...args);
} finally {
actions.dec();
}
}
catch
ブロックがなくても、Workspace
がエラーをスローすれば finally
ブロックは実行されますし、エラーはそのまま伝播します。したがって、このケースでも no-useless-catch
ルールは有効に機能します。
ESLint の no-useless-catch
ルールは、その名の通り「無駄な catch
ブロック」を指摘するルールです。このルールが発動する際によくある状況と、その解決策について見ていきましょう。
エラーをログに出力するだけなのに再スローしていないケース
よくある誤解
catch
ブロックで console.error
などを使ってエラーをログに出力しているから、これは「何か処理をしている」と認識されるはず、という誤解です。
// 問題のコード
try {
doSomethingDangerous();
} catch (error) {
console.error("エラーが発生しました:", error.message);
// throw error; がない
}
ESLint の指摘
この場合、no-useless-catch
ではなく、no-empty-catch
や no-console
などの別のルールが指摘する可能性があります。しかし、もしこの catch
ブロックにthrow error;
が含まれていた場合、no-useless-catch
が指摘する可能性があります。
トラブルシューティング
-
- エラーを握りつぶしたい場合
console.error
を使ってログに出力するだけで、それ以上エラーを伝播させたくない場合は、このコードで問題ありません。ただし、エラーを握りつぶすことは、予期せぬ挙動につながる可能性があるため、本当にその意図で良いのか再確認が必要です。 - エラーをログに出力し、かつ再スローしたい場合
console.error
の後にthrow error;
を追加します。この場合、ESLint はno-useless-catch
の警告を出しません。
try { doSomethingDangerous(); } catch (error) { console.error("エラーが発生しました:", error.message); throw error; // これで useless ではなくなる }
- エラーを握りつぶしたい場合
finally ブロックと組み合わせた誤解
よくある誤解
try...catch...finally
構文で、finally
ブロックで必ず実行したい処理があるため、catch
ブロックでエラーを再スローするのは仕方がない、という誤解です。
// 問題のコード
async function fetchData() {
let connection;
try {
connection = await establishConnection();
const data = await connection.query("SELECT * FROM users");
return data;
} catch (error) {
// この catch ブロックが no-useless-catch に指摘される
throw error;
} finally {
if (connection) {
await connection.close(); // 必ず接続を閉じる
}
}
}
ESLint の指摘
上記の例では、catch (error) { throw error; }
の部分が no-useless-catch
に指摘されます。
トラブルシューティング
-
finally ブロックの特性を理解する
JavaScript のtry...finally
構文では、try
ブロック内でエラーが発生した場合でも、finally
ブロックは必ず実行されます。エラーはfinally
ブロックの実行後に自動的に呼び出し元に伝播します。したがって、上記のコードは次のように書き換えることができます。
async function fetchData() { let connection; try { connection = await establishConnection(); const data = await connection.query("SELECT * FROM users"); return data; } finally { // catch ブロックは不要 if (connection) { await connection.close(); } } }
この形にすることで、
no-useless-catch
の指摘はなくなります。
ロギングライブラリやフレームワークの特殊なエラーハンドリング
よくある誤解
特定のロギングライブラリやフレームワークが、エラーをキャッチした後に内部的に追加の処理(例えば、エラーレポートサービスへの送信など)を行うため、throw error;
が必要だ、というケースです。
// 問題のコード (特定のライブラリを想定)
try {
processData();
} catch (error) {
// ライブラリの関数を呼び出し、それが内部で throw している、あるいはそう見える
MyLoggingLibrary.logAndRethrow(error); // これが内部で throw error; しているかもしれない
}
ESLint の指摘
もし MyLoggingLibrary.logAndRethrow(error);
の実体が単なる throw error;
のラッパーである場合、no-useless-catch
が指摘する可能性があります。ESLint は関数の内部実装までは追跡しないため、単なる関数呼び出しとして認識し、catch
ブロックが何もしていないと判断するからです。
トラブルシューティング
-
/* eslint-disable no-useless-catch */ を使う
上記のような本当に特殊なケースで、かつルールを無効化してもコードの可読性や保守性に問題がないと判断できる場合に限り、該当行またはブロックで ESLint のルールを一時的に無効化することを検討します。try { processData(); } catch (error) { /* eslint-disable-next-line no-useless-catch */ MyLoggingLibrary.logAndRethrow(error); }
ただし、これは最後の手段であり、無効化する理由をコメントで明確に記述することが強く推奨されます。
-
ESLint の設定で特定の関数を無視する
もし、そのcatch
ブロックが本当に必要で、no-useless-catch
が誤検知していると判断できる場合、ESLint の設定で特定の関数呼び出しパターンを無視するように設定することはできません。no-useless-catch
はコードの構造を見て判断するためです。 -
ライブラリ/フレームワークのドキュメントを確認する
そのライブラリのlogAndRethrow
のような関数が本当にエラーを再スローしているのか、あるいは内部でエラーを消費しているのかを確認します。
テストコードにおける意図的な再スロー
よくあるケース
テストコードにおいて、特定のエラーがスローされることを確認するために、catch
ブロックでエラーを捕捉し、それを再度スローしてテストフレームワークにエラーを認識させる場合があります。
// テストコードの例
test('should throw an error for invalid input', async () => {
try {
await validateInput(null);
fail('Expected an error but got none.'); // エラーがスローされない場合にテストを失敗させる
} catch (error) {
expect(error).toBeInstanceOf(Error);
expect(error.message).toBe('Invalid input');
throw error; // ここで no-useless-catch に指摘される可能性がある
}
});
ESLint の指摘
テストコードであっても、ESLint は一般的な JavaScript の構文規則に基づいて no-useless-catch
を指摘します。
トラブルシューティング
-
テストフレームワークの expect や toThrow を使う
多くのテストフレームワーク(Jest, Mocha + Chai など)には、特定のエラーがスローされることをアサートするための専用のAPIがあります。これらを使用することで、try...catch
ブロック自体を不要にすることができます。Jest の例
test('should throw an error for invalid input', async () => { await expect(validateInput(null)).rejects.toThrow('Invalid input'); });
Mocha + Chai の例
it('should throw an error for invalid input', async () => { await expect(async () => await validateInput(null)).to.be.rejectedWith('Invalid input'); });
このように専用のAPIを使うことで、コードがより簡潔になり、
no-useless-catch
の指摘もなくなります。
no-useless-catch
ルールは、コードのシンプルさと保守性を高めるためのものです。エラーが再スローされるだけの catch
ブロックは、通常、その存在価値がないことを示唆しています。ESLint の指摘を受けたら、まずは「なぜこの catch
ブロックが必要なのか?」を自問し、本当に必要なければコードをリファクタリングすることを検討しましょう。
このルールは、「キャッチしたエラーをそのまま再スローするだけの catch
ブロックは無駄である」と判断します。つまり、catch (error) { throw error; }
のようなパターンを検出します。
基本的な無駄な catch の例 (ESLint が警告を出すパターン)
// 例 1-1: 同期処理の場合
function processData(data) {
try {
if (!data) {
throw new Error("データがありません");
}
return data.toUpperCase();
} catch (error) {
// この catch ブロックは ESLint によって「無駄」と指摘されます。
// なぜなら、エラーをキャッチして、そのまま再スローしているだけで、
// 何も特別な処理をしていないからです。
throw error;
}
}
// 例 1-2: 非同期処理 (Promise) の場合
async function fetchDataFromAPI() {
try {
const response = await fetch("https://example.com/api/data");
if (!response.ok) {
throw new Error(`HTTP エラー: ${response.status}`);
}
return await response.json();
} catch (err) {
// 非同期処理でも同様に指摘されます。
throw err;
}
}
解説
これらの例では、try
ブロック内でエラーが発生した場合、catch
ブロックがそのエラーを捕捉しますが、その後、何も追加の処理をせずに再び throw error;
でエラーをスローしています。
このような catch
ブロックは、コードの動作に何の影響も与えません。catch
ブロックがなくても、try
ブロックで発生したエラーは自動的に呼び出し元に伝播するためです。
no-useless-catch ルール適用後の推奨される書き方 (ESLint が警告を出さないパターン)
上記の「無駄な catch
」は、catch
ブロック自体を削除することで解決できます。
// 例 2-1: 同期処理の場合 (修正後)
function processData(data) {
if (!data) {
throw new Error("データがありません");
}
return data.toUpperCase();
// try...catch ブロック全体を削除しました。
// エラーは発生したらそのまま呼び出し元に伝播します。
}
// 例 2-2: 非同期処理 (Promise) の場合 (修正後)
async function fetchDataFromAPI() {
const response = await fetch("https://example.com/api/data");
if (!response.ok) {
throw new Error(`HTTP エラー: ${response.status}`);
}
return await response.json();
// try...catch ブロック全体を削除しました。
// エラーは発生したらそのまま呼び出し元に reject されます。
}
解説
catch
ブロックを削除しても、エラー伝播の動作は全く同じです。これにより、コードの行数が減り、より簡潔で読みやすくなります。
finally ブロックと組み合わせた場合の考慮事項
finally
ブロックがある場合でも、catch
ブロックが単にエラーを再スローするだけなら、それは無駄と見なされます。
// 例 3-1: finally ブロックと無駄な catch
async function cleanupExample() {
let resource;
try {
resource = await acquireResource(); // リソースを取得
await useResource(resource); // リソースを使用
} catch (error) {
// ESLint が指摘するパターン: catch で何もせず再スロー
throw error;
} finally {
if (resource) {
await releaseResource(resource); // 必ずリソースを解放する
}
}
}
解説
この場合、resource
の取得や使用中にエラーが発生しても、finally
ブロックは必ず実行されます。その後、エラーは自動的に呼び出し元に伝播します。したがって、catch (error) { throw error; }
はやはり不要です。
推奨される修正
// 例 3-2: finally ブロックのみを使用する (修正後)
async function cleanupExample() {
let resource;
try {
resource = await acquireResource();
await useResource(resource);
} finally { // catch ブロックを削除
if (resource) {
await releaseResource(resource);
}
}
}
解説
この形にすることで、リソースの解放が保証されつつ、無駄な catch
ブロックがなくなります。
no-useless-catch ルールが警告を出さない正当な catch の例
catch
ブロック内でエラーに対して何らかの追加の処理を行う場合、no-useless-catch
ルールは警告を出しません。
// 例 4-1: エラーをログに出力し、再スローする場合
function performCriticalOperation(config) {
try {
// 重要な処理
if (!config.isValid) {
throw new Error("設定が無効です");
}
// ...
} catch (error) {
console.error("クリティカルな操作中にエラーが発生しました:", error.message);
// エラーをログに出力した後、さらにエラーを呼び出し元に伝えるために再スロー
throw error; // これは「無駄」ではない
}
}
// 例 4-2: エラーから復旧したり、デフォルト値を返したりする場合
function parseUserInput(input) {
try {
const parsed = JSON.parse(input);
return parsed;
} catch (error) {
// JSON パースエラーが発生した場合、デフォルト値を返す(エラーを握りつぶす)
console.warn("ユーザー入力のパースに失敗しました。デフォルト値を返します。", error.message);
return {}; // エラーを再スローせず、別の値を返す
}
}
// 例 4-3: エラーを特定の型に変換して再スローする場合
class CustomError extends Error {}
function fetchDataWithCustomError() {
try {
// 外部サービスへの呼び出しなど
throw new Error("ネットワークエラー"); // 例としてエラーをスロー
} catch (error) {
// 汎用的なエラーをカスタムエラーに変換して再スロー
throw new CustomError(`データ取得に失敗しました: ${error.message}`);
}
}
解説
これらの例では、catch
ブロックが単なる再スロー以上の役割を果たしています。
- エラーの変換(カスタムエラーへのラップ)
- エラーからの復旧(デフォルト値の返却)
- ログ出力 (
console.error
)
このように、エラーに対して何らかの意味のある操作を行っている場合、no-useless-catch
ルールは catch
ブロックを「無駄」とは見なしません。
no-useless-catch
ルールが指摘するのは、catch (error) { throw error; }
のように、エラーを捕捉した後に何の追加処理もせずにそのまま再スローするだけの catch
ブロックです。このようなコードは、通常、以下に示す代替手法でより簡潔かつ意図が明確に書くことができます。
エラー伝播の活用(最も一般的で推奨される方法)
JavaScript のエラーは、try...catch
で明示的に捕捉されない限り、呼び出しスタックを自動的に伝播していきます。この特性を理解し、活用することが最も一般的な代替手法です。
悪い例 (no-useless-catch が指摘するコード)
function calculateSum(a, b) {
try {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new Error('両方の引数は数値である必要があります。');
}
return a + b;
} catch (error) {
throw error; // 無駄な catch
}
}
代替方法
try...catch
ブロックを削除し、エラーをそのまま伝播させる。
// 良い例: エラーをそのまま伝播させる
function calculateSum(a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new Error('両方の引数は数値である必要があります。');
}
return a + b;
}
// 呼び出し側でエラーハンドリングを行う
try {
const result = calculateSum(10, 'abc');
console.log(result);
} catch (e) {
console.error("エラーが発生しました:", e.message); // ここでエラーを処理
}
解説
calculateSum
関数内で発生したエラーは、try...catch
ブロックがなくても自動的に呼び出し元(この場合はグローバルスコープの try...catch
)に伝播します。これにより、コードがより簡潔になり、関数の責務(計算ロジック)とエラーハンドリングの責務が分離されます。
非同期処理 (async/await) における finally ブロックの活用
async/await
を使用する際、リソースの解放など、エラーの有無にかかわらず実行したい処理がある場合、finally
ブロックが非常に役立ちます。この場合、catch
ブロックは不要です。
悪い例 (no-useless-catch が指摘するコード)
async function processFile(filePath) {
let fileHandle;
try {
fileHandle = await openFile(filePath);
const content = await readFile(fileHandle);
// ... ファイルの内容を処理 ...
return content;
} catch (error) {
throw error; // 無駄な catch
} finally {
if (fileHandle) {
await closeFile(fileHandle); // 必ずファイルを閉じる
}
}
}
代替方法
catch
ブロックを削除し、finally
ブロックのみを使用する。
// 良い例: async/await と finally の組み合わせ
async function processFile(filePath) {
let fileHandle;
try {
fileHandle = await openFile(filePath);
const content = await readFile(fileHandle);
// ... ファイルの内容を処理 ...
return content;
} finally { // catch ブロックは不要
if (fileHandle) {
await closeFile(fileHandle); // エラーが発生しても必ず実行される
}
}
}
// 呼び出し側でエラーハンドリングを行う
async function main() {
try {
const data = await processFile("my_data.txt");
console.log("ファイル内容:", data);
} catch (e) {
console.error("ファイル処理中にエラー:", e.message);
}
}
main();
解説
try
ブロック内でエラーが発生した場合でも、finally
ブロックは実行され、その後エラーは自動的に processFile
の呼び出し元に伝播(Promise が reject)されます。これにより、コードがクリーンに保たれ、リソースの解放も保証されます。
テストフレームワークの専用アサーションの活用
テストコードでエラーがスローされることを確認する場合、try...catch
で捕捉して再スローするのではなく、テストフレームワークが提供する専用のメソッドを使用します。
悪い例 (no-useless-catch が指摘するテストコード)
// Jest の例
test('should throw error for invalid input', () => {
try {
validateInput(null);
// fail('Error was not thrown'); // Jest の場合は不要だが、一般的なテストフレームワークで使う場合も
} catch (error) {
expect(error.message).toBe('Invalid input');
throw error; // ここで no-useless-catch に指摘される可能性がある
}
});
代替方法
テストフレームワークの toThrow
や rejects.toThrow
などを使用する。
// 良い例: Jest の toThrow/rejects.toThrow を使用
test('should throw error for invalid input', () => {
// 同期関数の場合
expect(() => validateInput(null)).toThrow('Invalid input');
});
test('should reject with error for async function', async () => {
// 非同期関数の場合
await expect(async () => await asyncValidateInput(null)).rejects.toThrow('Async error');
});
解説
Jest などのテストフレームワークは、特定のコードがエラーをスローすることをアサートするための強力な機能を提供しています。これらを使用することで、冗長な try...catch
ブロックが不要になり、テストコードがより簡潔で意図が明確になります。
エラーの変換や追加情報の付与(正当な catch の利用)
これは no-useless-catch
の代替というよりは、正当な catch
ブロックの利用例です。catch
ブロックが、エラーをそのまま再スローするのではなく、何らかの変換や付加価値を与える場合に用いられます。
正当な catch の例
// エラーをより具体的なカスタムエラーに変換する
class NotFoundError extends Error {}
function getUserById(id) {
try {
const user = database.findUser(id);
if (!user) {
throw new Error("ユーザーが見つかりません"); // 汎用的なエラー
}
return user;
} catch (error) {
// 汎用的なエラーを、より意味のあるカスタムエラーに変換
if (error.message.includes("ユーザーが見つかりません")) {
throw new NotFoundError(`ID ${id} のユーザーが見つかりませんでした。`);
}
// その他のエラーはそのまま再スロー(あるいはログ記録など)
throw error; // これは無駄ではない(変換処理があるため)
}
}
解説
この場合、catch
ブロックは単なる再スローではなく、エラーの種類を識別し、よりアプリケーション固有の NotFoundError
に変換しています。このような catch
ブロックは no-useless-catch
の対象外であり、正当なエラーハンドリング手法です。