JavaScriptでPromiseの.then(success, fail)はいつアンチパターンになるのか?
JavaScript、Node.js、Promiseにおける「.then(success, fail)」のアンチパターンについて
JavaScript、Node.js、Promiseにおける.then(success, fail)
は、非同期処理の処理結果を受け取るための一般的な方法です。しかし、状況によっては、.then(success, fail)
の使用がアンチパターンと見なされる場合があります。
アンチパターンとなるケース
以下のようなケースにおいて、.then(success, fail)
の使用はアンチパターンと見なされます。
- 非同期処理のネスト: 複数の非同期処理を連鎖的に実行する場合、
.then(success, fail)
をネストさせるとコードが冗長になり、可読性が低下します。 - エラー伝播の阻害:
fail
コールバック関数内でエラーを再スローしないと、エラーが上位のコードまで伝播されず、問題の特定が難しくなります。 - エラー処理の複雑化:
fail
コールバック関数を使用すると、エラー処理コードが複雑になり、読みづらくなる可能性があります。
代替手段
.then(success, fail)
の使用を避けるためには、以下の代替手段を検討することができます。
- 非同期処理の抽象化: 非同期処理を関数やクラスに抽象化することで、コードをよりモジュール化し、テストしやすくなります。
- Promiseチェーンの使用: 複数の非同期処理を連鎖的に実行する場合、Promiseチェーンを使用することで、コードをより簡潔に記述することができます。
catch
ブロックの使用: エラー処理はcatch
ブロックで行うことで、コードをよりシンプルに保ち、エラー伝播を確実にすることができます。
例
以下のコードは、.then(success, fail)
の使用を避けたエラー処理の例です。
async function fetchData() {
try {
const response = await fetch('https://example.com/data');
const data = await response.json();
return data;
} catch (error) {
console.error(error);
throw error; // エラーを上位のコードまで伝播
}
}
fetchData().then(data => {
console.log(data);
}).catch(error => {
console.error(error);
});
function loadUserData(userId) {
return new Promise((resolve, reject) => {
// 非同期処理を行う
setTimeout(() => {
if (userId === 1) {
const userData = {
name: 'Taro Yamada',
email: '[email protected]'
};
resolve(userData);
} else {
reject(new Error('Invalid user ID'));
}
}, 1000);
});
}
loadUserData(1)
.then(userData => {
console.log(userData.name); // Taro Yamada
}, error => {
console.error(error); // Error: Invalid user ID
});
問題点:
- エラーが発生した場合、エラーメッセージのみが出力され、原因の特定が困難。
- エラー処理が
fail
コールバック関数内に記述されており、コードが冗長で読みづらい。
改善案:
async function loadUserData(userId) {
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
if (!response.ok) {
throw new Error(`Failed to load user data: ${response.status}`);
}
const userData = await response.json();
return userData;
} catch (error) {
console.error(error);
throw error; // エラーを上位のコードまで伝播
}
}
loadUserData(1)
.then(userData => {
console.log(userData.name); // Taro Yamada
})
.catch(error => {
console.error(error.message); // Failed to load user data: 404
});
- エラーメッセージに加え、ステータスコードも出力することで、問題の特定を容易にする。
- エラー処理を
catch
ブロックで行うことで、コードをよりシンプルに保ち、エラー伝播を確実にする。
Promiseチェーンの例
async function loadUserAndGreeting(userId) {
const userData = await loadUserData(userId);
const greeting = await generateGreeting(userData.name);
return {
userData,
greeting
};
}
loadUserAndGreeting(1)
.then(result => {
console.log(result.userData.name); // Taro Yamada
console.log(result.greeting); // こんにちは、Taro Yamadaさん!
})
.catch(error => {
console.error(error);
});
説明:
- Promiseチェーンを使用することで、コードをより簡潔に記述することができます。
loadUserAndGreeting
関数は、非同期処理を2つ連鎖的に実行します。
非同期処理の抽象化の例
class UserService {
async loadUser(userId) {
const response = await fetch(`https://api.example.com/users/${userId}`);
if (!response.ok) {
throw new Error(`Failed to load user data: ${response.status}`);
}
const userData = await response.json();
return userData;
}
}
const userService = new UserService();
userService.loadUser(1)
.then(userData => {
console.log(userData.name); // Taro Yamada
})
.catch(error => {
console.error(error);
});
- コードをモジュール化し、テストしやすくなります。
UserService
クラスは、ユーザーデータの読み込みに関する非同期処理を抽象化します。
catchブロックの使用
catch
ブロックは、非同期処理中に発生したエラーを処理するために使用されます。.then(success, fail)
のfail
コールバック関数と同様の機能を提供しますが、以下の利点があります。
- エラー伝播の確実化: エラーが発生した場合、
catch
ブロック内で再スローしないと、エラーが上位のコードまで伝播されず、問題の特定が難しくなります。 - コードの簡潔化: エラー処理コードを
then
ブロックとは独立して記述できるため、コード全体の可読性が向上します。
async function fetchData() {
try {
const response = await fetch('https://example.com/data');
const data = await response.json();
return data;
} catch (error) {
console.error(error);
throw error; // エラーを上位のコードまで伝播
}
}
fetchData().then(data => {
console.log(data);
}).catch(error => {
console.error(error);
});
- エラーが発生した場合、
console.error
でエラーメッセージを出力し、throw error
でエラーを上位のコードまで伝播させています。 - 上記のコードは、
.then(success, fail)
を使用せずにcatch
ブロックでエラー処理を行っています。
利点:
- エラー伝播が確実化される。
- コードが簡潔で読みやすい。
Promiseチェーンは、複数の非同期処理を連鎖的に実行するために使用されます。.then(success, fail)
をネストさせる代わりに、Promiseチェーンを使用することで、以下の利点があります。
- エラー処理の明確化: 各非同期処理におけるエラー処理を個別に記述できるため、エラー処理の流れが明確になります。
- コードの簡潔化: ネスト構造を解消することで、コードがより読みやすく、理解しやすくなります。
async function loadUserAndGreeting(userId) {
try {
const userData = await loadUserData(userId);
const greeting = await generateGreeting(userData.name);
return {
userData,
greeting
};
} catch (error) {
console.error(error);
throw error; // エラーを上位のコードまで伝播
}
}
loadUserAndGreeting(1)
.then(result => {
console.log(result.userData.name); // Taro Yamada
console.log(result.greeting); // こんにちは、Taro Yamadaさん!
})
.catch(error => {
console.error(error);
});
- 各非同期処理におけるエラー処理は、
catch
ブロックで個別に記述されています。 - 上記のコードは、Promiseチェーンを使用して、ユーザーデータの読み込みと挨拶の生成という2つの非同期処理を連鎖的に実行しています。
- エラー処理の流れが明確。
非同期処理の抽象化
非同期処理を関数やクラスに抽象化することで、以下の利点があります。
- テストの容易化: 抽象化された非同期処理を個別にテストしやすくなります。
- コードのモジュール化: 非同期処理を独立したモジュールとして記述することで、コード全体の構造が明確になり、保守性が高まります。
class UserService {
async loadUser(userId) {
const response = await fetch(`https://api.example.com/users/${userId}`);
if (!response.ok) {
throw new Error(`Failed to load user data: ${response.status}`);
}
const userData = await response.json();
return userData;
}
}
const userService = new UserService();
userService.loadUser(1)
.then(userData => {
console.log(userData.name); // Taro Yamada
})
.catch(error => {
console.error(error);
});
- 上記のコードは、
UserService
クラスを使用して、ユーザーデータの読み込みという非同期処理を抽象化しています。
javascript node.js promise