TypeScript 非同期処理を一気通貫で同期処理として約束(Promise)させる

はじめに - Javaエンジニアの苦悩。JavaScriptの非同期処理にはまる -

JavaScriptの非同期処理、await を書いているのになぜかスルーされる……。そんな経験はありませんか?

わたしは数年前、Javaアプリケーション(アプレットだけど)をTypeScriptに移植する際、
この『止まらない非同期』に数週間も頭を抱えました。
結論から言うと、非同期処理を同期処理のように動かすには、
最下層から最上層まで**『一気通貫』で約束(Promise)を一気通貫で繋ぎ切る必要がありました。

今回の記事は、私が泥臭い試行錯誤の末にたどり着いた、非同期リレーの鉄則を共有します。

状況とJavaプログラマからの感覚

JavaからJavaScript(実際にはTypeScriptだけど)に書き換えをしていくとき、
通信処理部分が非同期処理になる。という話はインフラチームからは聞いていました。

現状、Javaでは同期処理となっているので、その通りにする必要がありました。
その解決策として、Promise(約束)を知りました。

その当時の各サイトの情報や、他チームのソースコードを見て真似て書いてみました。

まずは、サーバー通信処理への部分イメージ


function サーバー通信を待つ関数() {

	return new Promise((resolve, reject) => {

		// 1. 通信を投げる
		通信処理を投げる();

		// 2. 終わったかどうかを定期的にチェックする(ポーリング)
		const timer = setInterval(() => {
			if (通信成功した?) {
				clearInterval(timer);
				resolve("成功データ");
			} else if (タイムアウトした?) {
				clearInterval(timer);
				reject("エラー");
			}
		}, 100); // 100ミリ秒ごとに確認

	});
}

【Javaエンジニアの視点で】

実際には、手探り状態で必死でした。
「Promiseというものを使えばいいらしい」
「setIntervalで監視すれば待てるはずだ」……。
慣れないTypeScriptと格闘しながら、ようやく書き上げたこの最下層の関数。

「これだけロジックを組んだんだ。
これでもう、Javaの同期処理と同じように、
この関数を呼んだところでプログラムは一旦止まり、
データが返ってくるのを待ってくれるはずだ」

そう、一番下(最下層)さえ「待てる構造」に作り替えれば、
上層階のプログラムたちは何も知らなくても恩恵に預かれると、
本気で信じていたのです。

上層部は「約束」を無視して走り去っていく。。。

最下層プログラムを作りきり、
上位メソッドは現行Javaのロジックを移植して、いざ、処理を呼び出してみました。

フロントエンドは変わっているので、
A(GUI)→B→C(最下層の共通処理)でいうと、Bの部分は移植でいけました。
Aはもちろん新規作成です。

Javaエンジニアの感覚なら、通信が終わるまで止まってくれるはずです。

Bの通信処理


export class B {

	public 通信処理B():返信データ {
・・・
		// Cが終わるまでここで「待つ」
		let 返信データ = C.サーバー通信を待つ関数();

		return 返信データ;

	}

}

Aのイベント処理(GUI)

イメージとしてはBと変わりはありません。


export class A {

	public イベント処理A() {
・・・
		// Bが終わるまでここで「待つ」
		let 返信データ = B.通信処理B();

		// ここでようやく本物のデータを使って貼り付けができる
		this.データ貼り付け処理(data);

	}

}

これが止まらないんです。
データ貼り付け処理が先に動いてしまうんです。
ここからなぜなぜ地獄にハマりました・・・汗。

解決:最上層から最下層まで「一気通貫」で繋ぎ切る

結論から言うと、JavaScript/TypeScriptの世界で同期処理を実現するには、
**「バトン(Promise)を受け取る全員が、前の走者を待つ(await)」**
というルールを徹底するしかありません。

一箇所でも await を忘れたり、async を抜かしたりすれば、
その瞬間に「一気通貫」は崩壊し、プログラムは走り去ってしまいます。

Bの修正:リレーの中継地点

まずは中間の「B」です。 ここが一番の盲点でした。
自分も「約束(Promise)」を返すように宣言し、下層の結果をしっかり待ちます。


export class B {

	// 1. asyncを付けて「非同期(約束を返す)メソッド」であることを宣言
	// 2. 戻り値を Promise<返信データ> に包む
	public async 通信処理B():Promise<返信データ> {
・・・
		// 3. await を付けて、Cが終わるまでここで「止まって待つ」
		let 返信データ = await C.サーバー通信を待つ関数();

		return 返信データ;

	}

}

Aの修正:一気通貫のゴール地点

そして最上層の「A」です。
ここも同様に「待ち」の姿勢を入れなければなりません。


export class A {

	public async イベント処理A() {
・・・
		// 4. ここでも await。Bが結果を持ってくるまで次の行へ行かせない!
		let 返信データ = await B.通信処理B();

		// 5. ここでようやく、本物のデータ手に入れることができ、データ貼り付けができる
		this.データ貼り付け処理(data);

	}

}

まとめ:数週間かけて分かった「一気通貫」の正体

Javaエンジニアの私にとって、
最大の誤算は**「一番下が待てば、上も勝手に待つだろう」**という思い込みでした。

JavaScriptの世界はシビアで厳しいものでした。
「待ってほしいなら、全員が『待つ(await)』と宣言し、
バトン(Promise)を繋ぎ続けなければならない」。

この「一気通貫」の鎖さえ完成すれば、
非同期処理はまるで魔法のように、
Javaの同期処理と同じような顔をして動いてくれるようになります。

もし、あなたの await が無視されているなら、
どこかでその鎖が切れていないか、
数珠つなぎを一から辿ってみてください。

注意! たとえAとCが完璧でも、
中間のBが await を一回忘れるだけで、
Aの手元にはまた「空っぽの箱(Pending状態のPromise)」が届くことになります。

参考情報

Promise を使う(Promise チェーンの解説)
https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Using_promises

「async/await」の公式解説
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/async_function

#TypeScript #async-await

シェアしていただけるとうれしいです。

投稿者:

kinchannn

きんです。 よろしくおねがいいたします。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA