PromiseとRxJSの違い
概要
JavaScript/TypeScriptにおける非同期処理を扱う主要なツールとして、 PromiseとRxJS(Observable) があります。両者は似た目的で使用されることがありますが、設計思想とユースケースが大きく異なります。
このページでは、PromiseとRxJSの違いを理解し、どちらを使うべきかを判断するための情報を提供します。
基本的な違い
| 項目 | Promise | RxJS (Observable) |
|---|---|---|
| 標準化 | JavaScript標準(ES6/ES2015) | サードパーティライブラリ |
| 発行する値 | 単一の値 | 0個以上の複数の値 |
| 評価 | Eager(作成時に即実行) | Lazy(購読時に実行) |
| キャンセル | 不可[1] | 可(unsubscribe()) |
| 再利用 | 不可(結果は1度だけ) | 可(何度でも購読可能) |
| 学習コスト | 低い | 高い(オペレーターの理解が必要) |
| ユースケース | 単一の非同期処理 | 複雑なストリーム処理 |
コード比較: 単一の非同期処理
Promise
// Promiseは作成時に即実行される(Eager)
const promise = fetch('https://jsonplaceholder.typicode.com/posts/1')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error(error));Promiseは定義した瞬間に実行が始まります(Eager評価)。
RxJS
import { from } from 'rxjs';
import { switchMap, catchError } from 'rxjs';
import { of } from 'rxjs';
// Observableは購読するまで実行されない(Lazy)
const observable$ = from(fetch('https://jsonplaceholder.typicode.com/posts/1')).pipe(
switchMap(response => response.json()), // response.json()はPromiseを返すのでswitchMapを使用
catchError(error => {
console.error(error);
return of(null);
})
);
// 購読して初めて実行される
observable$.subscribe(data => console.log(data));RxJSは subscribe() が呼ばれるまで実行されません (Lazy評価)。同じObservableを複数回購読すると独立した実行が行われ、unsubscribe() で処理を中断できます。
TIP
実務での使い分け
- 即座に実行したい単発の処理 → Promise
- 必要なタイミングで実行したい、または複数回実行したい処理 → RxJS
コード比較: 複数の値を扱う場合
PromiseとRxJSの最も大きな違いの一つが、発行できる値の数です。Promiseは単一の値しか返せませんが、RxJSは複数の値を時系列で発行できます。
Promiseでは不可能
Promiseは一度しか解決できません。
// Promiseは単一の値しか返せない
const promise = new Promise(resolve => {
resolve(1);
resolve(2); // この値は無視される
resolve(3); // この値も無視される
});
promise.then(value => console.log(value));
// 出力: 1(最初の値のみ)最初の resolve() で値が確定すると、それ以降の resolve() は無視されます。
RxJSでは可能
Observableは何度でも値を発行できます。
import { Observable } from 'rxjs';
// Observableは複数の値を発行できる
const observable$ = new Observable(subscriber => {
subscriber.next(1);
subscriber.next(2);
subscriber.next(3);
subscriber.complete();
});
observable$.subscribe(value => console.log(value));
// 出力: 1, 2, 3next() を呼ぶたびに、購読者に値が届きます。すべての値を発行した後は complete() で完了を通知します。この特性により、リアルタイム通信、ストリーミングデータ、連続的なイベント処理など、時系列で変化するデータを自然に扱えます。
NOTE
実務での応用例
- WebSocketのメッセージ受信
- キーボード入力の逐次処理
- サーバーからのイベントストリーム(SSE)
- センサーデータの継続的な監視
キャンセルの比較
長時間かかる処理や、不要になった非同期処理をキャンセルできるかどうかは、リソース管理とユーザー体験の観点で重要です。PromiseとRxJSでは、キャンセル機能に大きな違いがあります。
Promise(キャンセル不可)
Promiseには標準的なキャンセル機能がありません。
const promise = new Promise(resolve => {
setTimeout(() => resolve('完了'), 3000);
});
promise.then(result => console.log(result));
// この処理をキャンセルする標準的な方法はない一度実行が始まると完了するまで止められず、メモリリークやパフォーマンス低下の原因になります。
WARNING
AbortController についてfetch() などのWeb APIは AbortController を使ってキャンセルできますが、これはPromise自体の機能ではなく、個別のAPIが提供する仕組みです。すべての非同期処理で使えるわけではありません。
RxJS(キャンセル可能)
RxJSは unsubscribe() でいつでもキャンセルできます。
import { timer } from 'rxjs';
const subscription = timer(3000).subscribe(
() => console.log('完了')
);
// 1秒後にキャンセル
setTimeout(() => {
subscription.unsubscribe(); // キャンセル
console.log('キャンセルしました');
}, 1000);
// 出力: キャンセルしました(「完了」は出力されない)購読を解除すると進行中の処理が即座に停止し、メモリリークを防げます。
TIP
実務でのキャンセル活用例
- ユーザーが画面を離れたときにHTTPリクエストをキャンセル
- 古い検索クエリの結果を破棄して、最新のクエリだけ処理(
switchMap) - コンポーネント破棄時に、すべてのObservableを自動的にキャンセル(
takeUntilパターン)
どちらを選ぶべきか
PromiseとRxJSのどちらを使うべきかは、処理の性質とプロジェクトの要件によって変わります。以下の基準を参考に、適切なツールを選択しましょう。
Promiseを選ぶべき場合
以下の条件に当てはまる場合は、Promiseが適しています。
| 条件 | 理由 |
|---|---|
| 単一の非同期処理 | APIリクエスト1回、ファイル読み込み1回など |
| シンプルなワークフロー | Promise.all, Promise.raceで十分 |
| 小規模プロジェクト | 依存関係を最小限にしたい |
| 標準APIのみ使用 | 外部ライブラリを避けたい |
| 初心者向けコード | 学習コストを抑えたい |
単一のAPIリクエスト:
interface User {
id: number;
name: string;
email: string;
username: string;
}
async function getUserData(userId: string): Promise<User> {
const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`);
if (!response.ok) {
throw new Error('ユーザーデータの取得に失敗しました');
}
return response.json();
}
// 使用例
getUserData('1').then(user => {
console.log('ユーザー名:', user.name);
console.log('メール:', user.email);
});このコードは、単一のユーザー情報を取得する典型的なパターンです。async/await を使うことで、同期的なコードのように読みやすく書けます。エラーハンドリングも try/catch で統一でき、シンプルで直感的です。
複数の非同期処理を並列実行:
interface Post {
id: number;
userId: number;
title: string;
body: string;
}
async function loadAllData(): Promise<[User[], Post[]]> {
const [users, posts] = await Promise.all([
fetch('https://jsonplaceholder.typicode.com/users').then(r => r.json()),
fetch('https://jsonplaceholder.typicode.com/posts').then(r => r.json())
]);
return [users, posts];
}
// 使用例
loadAllData().then(([users, posts]) => {
console.log('ユーザー数:', users.length);
console.log('投稿数:', posts.length);
});Promise.all() を使うことで、複数のAPIリクエストを並列に実行し、すべてが完了するのを待つことができます。これは初期データ読み込みなどで非常に便利です。一つでも失敗すると全体がエラーになる点に注意が必要ですが、そのシンプルさゆえに理解しやすく、メンテナンスも容易です。
RxJSを選ぶべき場合
以下の条件に当てはまる場合は、RxJSが適しています。
| 条件 | 理由 |
|---|---|
| 連続的なイベント処理 | マウス移動、キーボード入力、WebSocketなど |
| 複雑なストリーム処理 | 複数のイベントソースの結合や変換 |
| キャンセルが必要 | リソース管理を細かく制御したい |
| リトライ・タイムアウト | エラー処理を柔軟に行いたい |
| Angularプロジェクト | RxJSがフレームワークに統合されている |
| リアルタイムデータ | データが継続的に更新される |
具体例
import { fromEvent } from 'rxjs';
import { debounceTime, map, distinctUntilChanged, switchMap } from 'rxjs';
const label = document.createElement('label');
label.innerText = 'search: ';
const searchInput = document.createElement('input');
searchInput.type = 'input';
label.appendChild(searchInput);
document.body.appendChild(label);
// リアルタイム検索(オートコンプリート)
if (!searchInput) throw new Error('検索入力欄が見つかりません');
fromEvent(searchInput, 'input').pipe(
map(event => (event.target as HTMLInputElement).value),
debounceTime(300), // 300ms待ってから処理
distinctUntilChanged(), // 値が変わった時だけ処理
switchMap(query => // 最新のリクエストのみ実行
fetch(`https://api.github.com/search/users?q=${query}`).then(r => r.json())
)
).subscribe(results => {
console.log('検索結果:', results.items); // GitHub APIはitemsプロパティに結果を格納
});この例は、RxJSの真価が発揮される典型的なケースです。ユーザーの入力を監視し、300msの待機時間を設けて無駄なリクエストを減らし、値が変わったときだけ処理を行い、さらに最新のリクエストだけを有効にする(switchMap)ことで、古いリクエストの結果を自動的に破棄します。
IMPORTANT
Promiseだけでは困難な理由
- debounce(連続入力の制御)を手動実装する必要がある
- 古いリクエストのキャンセルを自分で管理しなければならない
- イベントリスナーのクリーンアップを忘れるとメモリリークが発生する
- 複数の状態(タイマー、フラグ、リクエスト管理)を同時に追跡する必要がある
RxJSでは、これらがすべて宣言的に、数行で実現できます。
PromiseとRxJSの相互運用
PromiseとRxJSは排他的なものではなく、相互に変換して組み合わせることができます。既存のPromiseベースのコードをRxJSのパイプラインに統合したり、逆にObservableを既存のPromiseベースのコードで使いたい場合に便利です。
PromiseをObservableに変換
RxJSは、既存のPromiseをObservableに変換するための複数の方法を提供しています。
from による変換
最も一般的な方法は from を使うことです。
import { from } from 'rxjs';
// Promiseを作成
const promise = fetch('https://jsonplaceholder.typicode.com/posts/1')
.then(response => response.json());
// from()でObservableに変換
const observable$ = from(promise);
observable$.subscribe({
next: data => console.log('データ:', data),
error: error => console.error('エラー:', error),
complete: () => console.log('完了')
});from() は、Promiseが解決すると1つの値を発行し、即座に complete します。エラーが発生すると error 通知が送られます。この変換により、Promise由来のデータに対しても、RxJSのオペレーター(map, filter, retry など)を自由に適用できるようになります。
defer による変換(遅延評価)
defer は、購読されるまでPromiseの作成を遅延させます。
import { defer } from 'rxjs';
// subscribe されるまで Promise は作成されない
const observable$ = defer(() =>
fetch('https://jsonplaceholder.typicode.com/posts/1').then(r => r.json())
);
// 購読ごとに新しい Promise を作成
observable$.subscribe(data => console.log('1回目:', data));
observable$.subscribe(data => console.log('2回目:', data));この方法は、購読するたびに新しいPromiseを作成したい場合に便利です。
ObservableをPromiseに変換
Observableから1つの値だけを取り出して、Promiseにすることができます。
firstValueFrom と lastValueFrom
RxJS 7以降では、以下の2つの関数が推奨されます。
| 関数 | 動作 |
|---|---|
firstValueFrom | 最初の値をPromiseとして返す |
lastValueFrom | 完了時の最後の値をPromiseとして返す |
import { of, firstValueFrom, lastValueFrom } from 'rxjs';
import { delay } from 'rxjs';
const observable$ = of(1, 2, 3).pipe(delay(1000));
// 最初の値をPromiseとして取得
const firstValue = await firstValueFrom(observable$);
console.log(firstValue); // 1
// 最後の値をPromiseとして取得
const lastValue = await lastValueFrom(observable$);
console.log(lastValue); // 3Observableが値を流す前に完了した場合、デフォルトではエラーになります。デフォルト値を指定することで回避できます。
WARNING
toPromise()は非推奨です。代わりにfirstValueFrom()またはlastValueFrom()を使用してください。
TIP
選択のガイドライン
firstValueFrom(): 最初の値だけが必要な場合(例: ログイン認証結果)lastValueFrom(): すべてのデータを処理した後の最終結果が必要な場合(例: 集計結果)
実践例:両者を組み合わせる
実際のアプリケーションでは、PromiseとRxJSを組み合わせて使用することが一般的です。
実務での注意事項
PromiseとObservableの混在は、設計の境界を明確にしないとアンチパターンに陥りやすいです。
よくある問題:
- キャンセル不能になる
- エラーハンドリングの分離
subscribe内でのawait(特に危険)- 同じデータを Promise と Observable で並行取得
詳しくは Chapter 10: PromiseとObservableの混在アンチパターン を参照してください。
フォーム送信とAPI呼び出し
ユーザーのフォーム送信イベントをRxJSで捕捉し、Fetch API (Promise) を使ってサーバーに送信する例です。
import { fromEvent, from } from 'rxjs';
import { exhaustMap, catchError } from 'rxjs';
import { of } from 'rxjs';
interface FormData {
username: string;
email: string;
}
// Promiseベースのフォーム送信
async function submitForm(data: FormData): Promise<{ success: boolean }> {
const response = await fetch('https://api.example.com/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) {
throw new Error('送信に失敗しました');
}
return response.json();
}
// RxJSでイベントストリームを管理
const submitButton = document.createElement('button');
submitButton.id = 'submit-button';
submitButton.innerText = '送信';
submitButton.style.padding = '10px 20px';
submitButton.style.margin = '10px';
document.body.appendChild(submitButton);
if (!submitButton) throw new Error('送信ボタンが見つかりません');
fromEvent(submitButton, 'click').pipe(
exhaustMap(() => {
const formData: FormData = {
username: 'testuser',
email: 'test@example.com'
};
// Promise関数をObservableに変換
return from(submitForm(formData));
}),
catchError(error => {
console.error('送信エラー:', error);
return of({ success: false });
})
).subscribe(result => {
if (result.success) {
console.log('送信成功');
} else {
console.log('送信失敗');
}
});フォーム送信ボタンがクリックされるたびに、新しい送信プロセスが開始されますが、送信中は新しい送信を無視します。
この例では、exhaustMap の使用により、送信中の重複リクエストを防いでいます。
検索オートコンプリート
入力フォームの値の変化を監視し、API検索を行う例です。
import { fromEvent, from } from 'rxjs';
import { debounceTime, switchMap, catchError } from 'rxjs';
import { of } from 'rxjs';
interface SearchResult {
items: Array<{
login: string;
id: number;
avatar_url: string;
}>;
total_count: number;
}
// Promise ベースのAPI関数
async function searchAPI(query: string): Promise<SearchResult> {
const response = await fetch(`https://api.github.com/search/users?q=${query}`);
if (!response.ok) {
throw new Error('検索に失敗しました');
}
return response.json();
}
// RxJSでイベントストリームを管理
const label = document.createElement('label');
label.innerText = 'search: ';
const searchInput = document.createElement('input');
searchInput.type = 'input';
label.appendChild(searchInput);
document.body.appendChild(label);
if (!searchInput) throw new Error('検索入力欄が見つかりません');
fromEvent(searchInput, 'input').pipe(
debounceTime(300),
switchMap(event => {
const query = (event.target as HTMLInputElement).value;
// Promise関数をObservableに変換
return from(searchAPI(query));
}),
catchError(error => {
console.error(error);
return of({ items: [], total_count: 0 }); // エラー時は空の結果を返す
})
).subscribe(result => {
console.log('検索結果:', result.items);
console.log('合計:', result.total_count);
});TIP
責務の分離による設計
- RxJS: イベント制御を担当(debounce、switchMapなど)
- Promise: HTTPリクエストを担当(async/await)
from(): 両者を橋渡し
各技術を適材適所で使い分けることで、コードの可読性と保守性が向上します。
メリットとデメリット
Promise
メリット
- JavaScript標準のため依存関係不要
async/awaitにより直感的で読みやすいコード- 学習コストが低い
- 単一タスクの処理がシンプル
デメリット
- 複数の値を扱えない
- キャンセル機能がない
- 連続的なストリーム処理には不向き
- 複雑なイベント処理が困難
RxJS
メリット
- 複数の値を時系列で扱える
- 豊富なオペレーターで複雑な処理が可能
- キャンセル(
unsubscribe)が簡単 - エラー処理やリトライを柔軟に実装可能
- 宣言的でテストしやすい
デメリット
- 学習コストが高い
- ライブラリへの依存が必要
- オーバーヘッドがある(小規模プロジェクトでは過剰)
- デバッグが難しい場合がある
RxJSが特に活躍する分野
RxJSは以下のような分野で特に強力です。Promiseだけでは実現が困難な複雑な要件を、エレガントに解決できます。
| 分野 | 具体例 | Promiseとの比較 |
|---|---|---|
| リアルタイム通信 | WebSocket、SSE、チャット、株価更新 | Promiseは単発の通信のみ。連続的なメッセージ処理には不向き |
| ユーザー入力制御 | 検索オートコンプリート、フォームバリデーション | debounce、distinctUntilChangedなどが標準装備 |
| 複数ソースの結合 | 検索条件×ソート順×フィルタの組み合わせ | combineLatest、withLatestFromで簡潔に記述可能 |
| オフライン対応 | PWA、ネットワーク状態監視、自動再同期 | retry、retryWhenで柔軟なリトライ制御 |
| ストリーミングAPI | OpenAI、AI応答のトークン逐次出力 | 連続データをリアルタイムで処理可能 |
| キャンセル制御 | 長時間処理の中断、古いリクエストの破棄 | unsubscribe()で即座にキャンセル可能 |
NOTE
RxJSの活用分野の詳細は、RxJSとは何か - ユースケースも参照してください。
まとめ
| 目的 | 推奨 | 理由 |
|---|---|---|
| 単一のHTTPリクエスト | Promise(async/await) | シンプルで読みやすく、標準API |
| ユーザー入力イベントの処理 | RxJS | debounce、distinctなどの制御が必要 |
| リアルタイムデータ(WebSocket) | RxJS | 連続的なメッセージを自然に扱える |
| 複数の非同期処理の並列実行 | Promise(Promise.all) | 単純な並列実行ならPromiseで十分 |
| 連続的なイベントストリーム | RxJS | 複数の値を時系列で扱える |
| キャンセル可能な処理 | RxJS | unsubscribe()で確実にキャンセル |
| シンプルなアプリケーション | Promise | 学習コストが低く、依存関係が少ない |
| Angularアプリケーション | RxJS | フレームワークに標準統合されている |
基本方針
- シンプルに済むならPromiseを使う
- 複雑なストリーム処理が必要ならRxJSを使う
- 両方を組み合わせるのも有効(
from()で橋渡し)
RxJSは強力ですが、すべての非同期処理にRxJSを使う必要はありません。適切なツールを適切な場面で使い分けることが重要です。
次のステップ
- Observableとは で Observable の詳細を学ぶ
- Creation Functions で Observable の作成方法を学ぶ
- Operators で Observable の変換と制御を学ぶ
AbortControllerを使えばPromiseベースの処理(fetchなど)のキャンセルは可能ですが、Promise自体の仕様にキャンセル機能はありません。 ↩︎