Skip to content

Node.js特有の落とし穴

Node.js特有の落とし穴と、他言語との違いを詳しく解説します。

1. トランザクション境界は明示的か?

Section titled “1. トランザクション境界は明示的か?”

特徴:

// Node.js: 明示的なトランザクション境界
async function createOrder(orderData: OrderData): Promise<Order> {
return await prisma.$transaction(async (tx) => {
// トランザクション内の処理を明示的に記述
const order = await tx.order.create({ data: orderData });
return order;
});
}

他言語との比較:

// Java: 宣言的トランザクション管理
@Transactional
public Order createOrder(OrderData orderData) {
// アノテーションでトランザクションを管理
Order order = orderRepository.save(new Order(orderData));
return order;
}

落とし穴:

  • トランザクションの見落とし: トランザクションを忘れると、データの整合性が保たれない
  • ネストしたトランザクション: ネストしたトランザクションの動作を理解する必要がある

特徴:

// Node.js: Promise/async-awaitによる非同期処理
async function fetchData(): Promise<Data> {
const data = await fetch('https://api.example.com/data');
return await data.json();
}

他言語との比較:

// Java: CompletableFutureによる非同期処理
CompletableFuture<Data> future = CompletableFuture.supplyAsync(() -> {
return externalApi.call();
});

落とし穴:

  • 未処理のPromise: Promiseが処理されないと、エラーが隠蔽される
  • エラーハンドリング: 適切なエラーハンドリングが必要

特徴:

// Node.js: 再実行は手動で実装する必要がある
async function processOrder(orderId: number): Promise<void> {
try {
await paymentService.chargePayment(orderId);
} catch (error) {
// リトライロジックを手動で実装
await retry(() => paymentService.chargePayment(orderId), 3);
}
}

他言語との比較:

// Java: @Retryableアノテーションで自動リトライ
@Retryable(maxAttempts = 3)
public PaymentResult chargePayment(Long orderId) {
return paymentApiClient.chargePayment(orderId);
}

落とし穴:

  • 再実行の実装漏れ: 再実行ロジックを実装し忘れると、一時的なエラーで処理が失敗する
  • 冪等性の確保: 再実行時に冪等性を確保する必要がある

4. after_commit的な逃げ道があるか?

Section titled “4. after_commit的な逃げ道があるか?”

Node.jsのトランザクションコミット後処理

Section titled “Node.jsのトランザクションコミット後処理”

特徴:

// Node.js: トランザクションコミット後の処理は手動で実装
async function createOrder(orderData: OrderData): Promise<Order> {
const order = await prisma.$transaction(async (tx) => {
return await tx.order.create({ data: orderData });
});
// トランザクションコミット後に外部APIを呼ぶ
await paymentService.chargePayment(order.id);
return order;
}

他言語との比較:

# Ruby (Rails): after_commitコールバック
class Order < ApplicationRecord
after_commit :call_external_api
def call_external_api
ExternalApi.call(self.id)
end
end

落とし穴:

  • トランザクションコミット後の処理: トランザクションコミット後の処理が失敗した場合、ロールバックできない
  • 一貫性の保証: トランザクションコミット後の処理が失敗した場合、データの不整合が発生する可能性がある

特徴:

// ❌ 問題: イベントループをブロック
const data = fs.readFileSync('large-file.txt', 'utf8'); // ブロッキング操作
// ✅ 解決: 非ブロッキング操作
const data = await fs.promises.readFile('large-file.txt', 'utf8');

他言語との比較:

// Java: マルチスレッドにより、ブロッキング操作でも他のリクエストを処理可能
public String readFile(String filePath) throws IOException {
return Files.readString(Paths.get(filePath)); // ブロッキング操作でも問題ない
}

落とし穴:

  • イベントループのブロック: 同期I/O操作により、イベントループがブロックされる
  • 他のリクエストの処理不可: イベントループがブロックされている間、他のリクエストが処理できない

Node.js特有の落とし穴のポイント:

  • トランザクション境界: 明示的なトランザクション境界、見落としに注意
  • 非同期処理: Promise/async-awaitによる非同期処理、未処理のPromiseに注意
  • 再実行: 手動で実装する必要がある、冪等性の確保が必要
  • after_commit的な逃げ道: トランザクションコミット後の処理は手動で実装、一貫性の保証が必要
  • イベントループのブロック: 同期I/O操作によりイベントループがブロックされる、非ブロッキング操作を使用

これらの落とし穴を理解することで、より安全なNode.jsアプリケーションを構築できます。