第19章:AsyncResult(Promise)の扱い方⚡🎁
(asyncでもResult設計を崩さないコツだよ〜😊🧡)
0. この章でできるようになること🎯✨
- async関数でも「成功/失敗」を Result で返して、読みやすさをキープできる🙂📦
- 「どこで await する?」「どこで catch する?」を迷わなくなる🧠🧭
- “Unhandled Rejection 地獄😱”を避ける設計の型が作れる🛡️✨
※ちなみに今のTypeScriptは 5.8 系が安定版として並び、5.9 は Beta 扱いのリリースが出てるよ(2026/01時点の公開情報)📌 (GitHub)
1. まず結論:AsyncResultってなに?🎁⚡
AsyncResult はひとことで言うとこれ👇
- Result:同期の「成功/失敗の箱」🎁
- AsyncResult:非同期版で「Promiseで包まれたResult」🎁⚡
![AsyncResult:Promiseで包まれたResult[(./picture/err_model_ts_study_019_promise_wrapper.png)
つまり…
- 成功:Promiseが「成功(resolve)」して、中身が Ok
- 失敗:Promiseが「成功(resolve)」して、中身が Err
- 例外:Promiseが「失敗(reject)」しちゃう(=事故りやすい)😱
ここが超重要ポイントだよ‼️
2. “reject”が増えると何がつらいの?😵💫💥
Promiseが reject されたのに拾われないと、環境によってはグローバルに “未処理” として扱われるよ⚠️
- ブラウザ:unhandledrejection イベントが飛ぶ🌐⚡ (MDN Web Docs)
- Node:process の unhandledRejection が飛ぶ🧨 (Node.js)
つまり「設計として想定してた失敗」なのに、実装ミスで未処理になった瞬間、運用事故に化けることがある…😇🧯
だからこの章の方針はこれ👇 ✅ 想定内の失敗(ドメイン/インフラ)は Err に寄せる ✅ 想定外(バグ/不変条件違反)は例外として落とす(境界でまとめて処理)
3. まず最小セットの型を作ろう🧩✨
export type Result<T, E> =
| { ok: true; value: T }
| { ok: false; error: E };
export const ok = <T>(value: T): Result<T, never> => ({ ok: true, value });
export const err = <E>(error: E): Result<never, E> => ({ ok: false, error });
export type AsyncResult<T, E> = Promise<Result<T, E>>;
この形にしておくと、条件分岐が超わかりやすい🙂🌈
4. AsyncResultの「事故らないルール」5つ🧸🛡️
ルール①:async関数は “原則 throw しない” 🙅♀️💥
- 想定内の失敗:Errで返す
- バグだけ throw(=Fail Fast)⚡🧱
ルール②:境界(入口/出口)だけは try/catch を置く🚪🧯
中で撒き散らさない。最後にまとめる✨
ルール③:awaitは「責任を持って拾う場所」でやる🧭
awaitしたら、その場で Result にして返す(または上に返す)🙂
ルール④:非同期合成は “ヘルパー” で読みやすくする🪄📚
if地獄を避ける🫠
ルール⑤:未処理rejectionの監視は保険として入れる👮♀️
ゼロにする設計を目指しつつ、最後の最後にログで気づけるようにする🧾✨ (Node/ブラウザの仕組みは公式が解説してるよ) (Node.js)
5. “tryCatchAsync” を作ると世界が平和になる🕊️✨
「Promiseがrejectする可能性がある処理」を、強制的に AsyncResult に変換しちゃう道具だよ🎁⚡
export const tryCatchAsync = async <T, E>(
f: () => Promise<T>,
onError: (e: unknown) => E
): AsyncResult<T, E> => {
try {
const v = await f();
return ok(v);
} catch (e: unknown) {
return err(onError(e));
}
};
これで、reject しがちなAPI(fetchとか外部SDKとか)を “Errに正規化” できる🙂🧼
6. 合成ヘルパー:andThenAsync(超よく使う)⛓️✨
「前の処理がOkなら次へ、Errならそのまま返す」やつ!
export const andThenAsync = async <T, E, U>(
ar: AsyncResult<T, E>,
f: (v: T) => AsyncResult<U, E>
): AsyncResult<U, E> => {
const r = await ar;
return r.ok ? f(r.value) : r;
};
これがあると、非同期3ステップでも読みやすい📖💗
7. ミニ実装:asyncな3ステップを Promise で成立させる🎓🧪
お題🎀
「ユーザーIDを受け取る → ユーザー取得 → 保存」みたいな流れを作るよ🙂
エラー型(例)
type DomainError =
| { kind: "UserNotFound"; userId: string }
| { kind: "InvalidUserId"; userId: string };
type InfraError =
| { kind: "Network"; message: string }
| { kind: "Db"; message: string; cause?: unknown };
① 入力チェック(同期だけど Result で返す)
const validateUserId = (userId: string): Result<string, DomainError> => {
if (!userId || userId.trim().length < 3) {
return err({ kind: "InvalidUserId", userId });
}
return ok(userId.trim());
};
② ユーザー取得(AsyncResult)
type User = { id: string; name: string };
const fetchUser = (userId: string): AsyncResult<User, DomainError | InfraError> =>
tryCatchAsync(
async () => {
// 例:外部I/Oは失敗しうる
const res = await fetch(`https://example.test/users/${userId}`);
if (res.status === 404) throw new Error("notfound");
if (!res.ok) throw new Error(`http ${res.status}`);
return (await res.json()) as User;
},
(e) => {
// ここは「正規化」する場所✨
const msg = e instanceof Error ? e.message : "unknown";
if (msg === "notfound") return { kind: "UserNotFound", userId } as const;
return { kind: "Network", message: msg } as const;
}
);
③ 保存(AsyncResult)
const saveUser = (user: User): AsyncResult<void, InfraError> =>
tryCatchAsync(
async () => {
// 例:DB保存のつもり
// await db.save(user)
},
(e) => {
const msg = e instanceof Error ? e.message : "unknown";
return { kind: "Db", message: msg, cause: e };
}
);
④ 3ステップ合成(読みやすい!)
export const registerFlow = async (
userId: string
): AsyncResult<void, DomainError | InfraError> => {
const v = validateUserId(userId);
if (!v.ok) return v; // 早期return✨(これ大事)
return andThenAsync(fetchUser(v.value), (user) => saveUser(user));
};
ポイントはここ👇😊
- 「入力ミス」は Result(同期)で返す
- 「外部I/O」は tryCatchAsync で Err にする
- “想定内の失敗” は reject じゃなく Err で運ぶ🎁
8. 例外との住み分け(ここ超テストに出る📌😆)
Resultで返すべきもの✅
- ユーザー入力ミス(ドメイン)📝
- 在庫なし/期限切れ(ドメイン)🛒
- ネットワーク/DB一時障害(インフラ)🌩️
例外で落としていいもの✅(=バグ)
- nullのはずがないのにnull(不変条件違反)🧱
- switchのdefaultに来た(設計漏れ)😇
- 型的にありえない値が来た(内部破壊)💥
9. “原因を失わない”小ワザ:causeチェーン🎁🧵
エラーを包むとき、原因を持たせるとデバッグが楽になるよ🙂 JavaScriptには Error.cause がある✨ (MDN Web Docs)
(Result設計でも「InfraErrorにcauseを載せる」だけで十分効くよ🧡)
10. ミニ演習📝✨(やると一気に身につくよ!)
演習A:tryCatchAsyncを自分の言葉で説明してみよう🧠💬
- 「rejectをErrに変える」ってどういうこと?
- どこに置くのが良い?(境界?ユースケース層?)
演習B:andThenAsyncをもう1個作る🪄
- mapAsync(Okのvalueだけ加工する)を実装してみてね🙂
演習C:落ち方パターンを網羅✅
「この処理が壊れるパターン」を10個書いて、
- Domain / Infra / Bug に分類
- AsyncResultで返す?throwする? を決める📋✨
11. AI活用🤖💖(Copilot/Codex向けプロンプト例)
- 「AsyncResult(Promise
)用に mapAsync / mapErrAsync / andThenAsync を型安全に実装して」 - 「この関数が reject しうる箇所を列挙して、AsyncResultに正規化する案を出して」
- 「DomainError/InfraErrorの境界が混ざってないかレビューして」👀
- 「未処理rejectionになりうる呼び出し方の事故例を3つ作って」😱
12. 章のまとめ🎀✨
- AsyncResult = Promise
で “想定内の失敗” を reject から守る🛡️ - tryCatchAsync があると「外部I/Oの失敗」を綺麗にErrへ寄せられる🧼
- 例外は “バグだけ” にして、境界でまとめて処理するのが気持ちいい🙂🚪
次の章(入力チェックは実行時だよ🧪🫥)に行くと、AsyncResult設計がさらに安定するよ〜!✨