C# の async/await が分かる人が TypeScript で詰まる5つ — Task と Promise の違い
みなさんこんにちは!ヒロポンです!
C# の async/await はもう手に馴染んでる。Task<T> を返して、await して、try/catch で例外を拾う。業務で何年も書いてきたやつ。
で、ある日「来週からフロント、TypeScript でよろしく」が降ってくる。
開いてみると、async/await がある。お、C# と同じやん??……と思って書くと、これが地味に違う。
Task と Promise、見た目はそっくり。なのに、開始タイミングも例外の消え方も並行の挙動も微妙にズレてる。
そのズレを知らずに書くと、本番で「例外が出てるはずなのに何も起きない」みたいな現象でハマります。
この記事は、C# の async/await を知ってる前提で、TypeScript の Promise で詰まる5箇所を「Task ↔ Promise の対応」で翻訳する。C# の感覚を捨てずに、ズレてる行だけ上書きするのが狙いです。
まず対応マップ: C# Task と TypeScript Promise
細かい話に入る前に、全体の対応をこんな感じで1枚に。

この表の各行を、コード付きで1つずつ見ていきます。
詰まり①: Promise は生成した瞬間に走り出す
C# だと、new Task(() => ...) で作った cold な Task は Start() するまで動きません。「作る」と「動かす」が分けられる。
TypeScript の Promise に、その分離はありません。new した瞬間に中身が走ります。
const p = new Promise<void>((resolve) => {
console.log("もう走ってる"); // ← new した時点で出力される
resolve();
});
console.log("Promise を作った後");
// 出力順: "もう走ってる" → "Promise を作った後"
誰も await していなくても、.then() を付けていなくても、中の処理は始まってる。MDN にも「executor は Promise が構築されると同時に同期的に呼ばれる」と明記されています。
「あとで実行したい」なら、Promise を関数で包んで遅延させます。
// これなら呼ぶまで走らない (cold の代用)
const lazy = () => fetch("/api/data");
// lazy() を呼んだ瞬間に初めて走る
C# の cold Task の感覚で「まだ走ってないはず」と思ってると、ここでズレます。
詰まり②: await を忘れると例外が消える
これがいちばん怖いやつ。
C# なら、Task を返すメソッドを await し忘れると、コンパイラが CS4014 の警告を出してくれます。気づける。
TypeScript は、await を忘れても警告が出ません。
しかも、中で投げた例外が同期の try/catch に引っかからずに消えます。これがタチ悪い。
async function risky(): Promise<void> {
throw new Error("失敗した");
}
function caller() {
risky(); // ← await を忘れた。例外はここで捕まらない
console.log("ここは普通に通る");
}
// "失敗した" は Unhandled promise rejection になり、
// 同期の try/catch には一切引っかからない
await を付ければ、いつもの try/catch で拾えます。
async function caller() {
try {
await risky(); // await して初めて例外が同期フローに乗る
} catch (e) {
console.log("捕まえた:", (e as Error).message);
}
}
「例外が出てるはずなのに何も起きない」の正体は、だいたいこの await 忘れ。C# の警告に守られてた身からすると、ここは無防備に感じます。
詰まり③: Promise.all は1つこけたら全体がこける
並行実行は、C# の Task.WhenAll に当たるのが Promise.all です。でも失敗時の挙動が違う。
Task.WhenAll は、全部のタスクが終わるのを待ってから、例外をまとめて投げる。
Promise.all は、1つでも reject した瞬間に即 reject(fail-fast)。残りの完了は待ちません。
try {
await Promise.all([ok(), fail(), ok()]);
// fail() が reject した瞬間にここを抜けて catch へ。
// 残りの ok() の完了は待たない
} catch (e) {
console.log("どれか1つこけた:", (e as Error).message);
}
「全部の結果(成功も失敗も)が欲しい」なら、Promise.allSettled を使う。これが感覚的には Task.WhenAll に近い。
const results = await Promise.allSettled([ok(), fail(), ok()]);
// results[i].status が 'fulfilled' か 'rejected'。全部の結末が取れる
for (const r of results) {
console.log(r.status);
}
実行結果:

Task.WhenAll のつもりで Promise.all を使うと、「片方こけたら、もう片方の結果が取れない」で詰まります。
詰まり④: async 関数は常に Promise を返す
C# の async Task<int> と同じで、TypeScript の async function は戻り値を常に Promise で包みます。
async function getValue(): Promise<number> {
return 42; // return 42 でも、呼び出し側が受け取るのは Promise<number>
}
const n = await getValue(); // await して初めて number になる
await を付け忘れると、n は number ではなく Promise<number> のまま。C# でも Task<int> を await せずに使うと型が合わないので、ここは地続きです。
注意は、C# の async void に当たる投げっぱなし(fire-and-forget)。
TS でも async function を await せず呼べます。ただしその場合、中の例外が②の通り消えます。
投げっぱなしにするなら、せめて .catch() を付けて例外だけは拾っておくのが安全です。
// 投げっぱなしにするなら .catch だけは付ける
doSomethingAsync().catch((e) => console.error("拾った:", e));
詰まり⑤: ConfigureAwait に当たるものは無い
C# でライブラリを書くとき、ConfigureAwait(false) を付けて UI スレッドのデッドロックを避ける、という定番作法がありますよね。
TypeScript(JavaScript)には、これに当たるものがありません。
理由は、JS が単一スレッドのイベントループで動いてるから。C# の SynchronizationContext(元のスレッドに戻る仕組み)がそもそも無いんですよね。
const res = await fetch("/api/data");
// await の後で「元のスレッドに戻る」という概念がない。
// だから ConfigureAwait のような指定も要らない
これは「JS の方が楽」という話じゃなくて、事情が違うだけ。
JS は単一スレッドなので、逆に CPU が重い処理を裏で並列に回したいときは、Worker という別の仕組みが要ります。C# のスレッドプール感覚はそのまま持ち込めない。一長一短です。
動かして確かめる
①と②は、実際に動かすとこんな感じで一発で腑に落ちます。tsx などで TypeScript を直接実行してみてください。
// check.ts — Promise の eager 実行と await 忘れを観察する
console.log("start");
new Promise<void>((resolve) => {
console.log("executor 実行");
resolve();
});
console.log("end");
// 出力: start → executor 実行 → end (executor が同期で走る)
npx tsx check.ts
実行結果:

executor 実行 が end より先に出れば、「Promise は生成即実行」が体感できます!!
俺の現場メモ
C# の Task 感覚のまま TS に入ると、まず②の await 忘れで一度は事故ります。
俺もやりました。例外が出てるはずのバッチが、エラーログに何も残さず静かに失敗してる。
え、なんで止まらへんの??ってなって、原因にたどり着くまでにけっこう時間を食った。ログには何も出てないんだから、そりゃ探すよなって。
でも、ハマったのは結局この5箇所くらい。残りの9割は「Task ↔ Promise」の対応で地続きでした。
C# で非同期を書いてきた経験は、ちゃんと効きます。ゼロから覚え直すわけじゃない。
おすすめは、こんな感じで手元に対応マップ(上の表)を置いて、ズレてる行だけ意識すること。
await 忘れに警告が出ない、Promise.all は fail-fast。この2つだけ体に入れておけば、TS の非同期で大コケすることはほぼ無くなります。
まとめ
C# の async/await 経験者が TypeScript で詰まる5つ、整理するとこうです。
- ① Promise は new した瞬間に走る — cold Task は無い。遅延は関数で包む
- ② await 忘れは警告ナシで例外が消える — Unhandled rejection。C# の CS4014 に守られてない
- ③ Promise.all は fail-fast — 全部待ちたいなら allSettled(こっちが Task.WhenAll 感覚)
- ④ async は常に Promise を返す — 投げっぱなしは
.catch()で例外だけ拾う - ⑤ ConfigureAwait は無い — JS は単一スレッドで SynchronizationContext が無いだけ
Task の知識は捨てなくていい。ズレてる5箇所だけ上書きすれば、TypeScript の非同期もいい感じに書けます。
よくある質問
Q1. C# の Task と TS の Promise、根本的に何が一番違う?
開始タイミングと await 忘れの安全網です。
Promise は new した瞬間に走り(cold が無い)、await を忘れても C# のような警告が出ません。例外は Unhandled rejection に消えます。
並行系(Task.WhenAll ↔ Promise.all)の失敗挙動も違うので、その3点を押さえれば大半は地続きです。
Q2. Promise.all と Promise.allSettled、どっちを使えばいい?
「1つでも失敗したら全体を失敗にしたい」なら Promise.all(fail-fast)。「成功も失敗も全部の結果を集めたい」なら Promise.allSettled です。
C# の Task.WhenAll の「全部終わるまで待つ」感覚に近いのは allSettled の方です。
Q3. await を付け忘れても動くなら、付けなくていい時もある?
戻り値も例外も要らない「投げっぱなし」なら付けなくても動きます。ただしその場合は .catch() で例外だけは拾ってください。
付けないと例外が Unhandled rejection になり、原因不明の静かな失敗になります。基本は await を付けるのが安全です。
Q4. C# の CancellationToken に当たるものは TS にある?
標準の Promise にキャンセルは組み込まれていません。fetch などでは AbortController を使って中断します。
C# の CancellationToken ほど統一された仕組みではないので、ライブラリごとのキャンセル手段を確認する形になります。
関連記事
- TypeScript for C#: LINQ と配列メソッドの対応表 — 同じ「C# の知識で TS を読む」シリーズ。LINQ ↔ 配列メソッドの対応
- C# WinForms で BackgroundWorker から async/await へ — C# 側の async/await・Task の基礎をおさらいしたい人向け
- C# の文字列比較 == / Equals / StringComparison でハマる3箇所 — C# の「同じ」の判定。TS の === とセットで押さえると混乱しない
- C# の using ステートメント3パターン — リソース解放の作法。async と並んで「型に守られる」C# らしさの話
- 「客先常駐しかない」業務SEが技術+αで抜ける道 — C# の知識を土台に TS まで広げられる SE は、案件の選択肢がどう変わるか。橋渡しのキャリア論
以上!
「C# の感覚で TS 書いて await 忘れた」経験ある人いたら、どんどんシェア待ってるぜ!!
執筆者
バイブス父さん — 業務SE 7年(SIer 正社員2 / フリーランス5)。現職は SEO 直轄部の AI アドバイザー兼 PL、副業で中小 SIer の CTO。SIer の正社員からフリーランスに転じ、複数のエージェント経由で案件を回してきた経験ベースで「業務SE視点」の技術 + キャリア記事を書いています。
🐦 X: @hiro_progra0524(日々の現場メモ更新中)
📝 About Me で経歴詳細を見る


コメント