- Day 28:非同期処理の世界に入ってみる ― Promise / async / await
- 非同期処理とは何かをざっくりイメージする
- Promiseとは何か ― 「未来の値」の約束
- async / awaitとは何か ― Promiseを「同期っぽく」書くための仕組み
- Step 1:User型を用意する
- Step 2:疑似API関数 getUser を設計する
- Step 3:getUserを呼び出してみる(await版)
- Step 4:エラー処理 ― try / catch で失敗に備える
- Step 5:Promiseの型を意識して書いてみる
- まとめ用テンプレート:Day 28の疑似API一式
- Day 28のまとめ
- Day 28:非同期処理演習 ― 成功・エラー・Loadingを全部扱ってみる
- 状態を3つに分けて考える:loading / success / error
- Step 1:User型と状態管理用の型を用意する
- Step 2:疑似APIを作る ― 成功も失敗もあり得る
- Step 3:状態を管理しながら非同期処理を実行する関数を書く
- Step 4:状態に応じて表示を切り替える
- Step 5:Loading状態をもう少し意識してみる
- まとめ用テンプレート:Day 28演習一式
- Day 28演習のまとめ
Day 28:非同期処理の世界に入ってみる ― Promise / async / await
第4週のDay 28では、いよいよ 非同期処理 に入っていきます。 WebアプリやAPI通信では、ほぼ必ず登場する重要テーマです。
扱うキーワードは次のとおりです。
- Promise
asyncawaittry/catch- API通信の基本
- Promiseの型
実践テーマはこうです。
疑似APIを作る。
async function getUser(): Promise<User> { ... }
「サーバーからデータを取ってくる」イメージを使いながら、 非同期処理の基本をステップバイステップでかみ砕いていきます。
非同期処理とは何かをざっくりイメージする
「すぐ終わらない処理」とどう付き合うか
プログラムの中には、次のような「すぐ終わらない処理」があります。
- ネットワーク越しにAPIからデータを取得する
- データベースにアクセスする
- ファイルを読み書きする
こうした処理は、結果が返ってくるまで時間がかかることが多いです。 その間、プログラム全体が止まってしまうと困るので、
「結果が返ってきたら教えてね」 「それまで他の処理を進めておくね」
というスタイルで付き合う必要があります。 このときに登場するのが Promise と async / await です。
Promiseとは何か ― 「未来の値」の約束
Promiseのざっくりしたイメージ
Promiseは、よくこう説明されます。
「まだ手元にないけれど、将来手に入る“かもしれない”値の約束」
たとえば、
- 「APIからユーザー情報を取ってくる」関数は、 今すぐ
Userを返せるわけではありません。 - 代わりに、「あとで
Userを返すよ」という Promise\<User\> を返します。
TypeScriptでは、Promiseの型はこう書きます。
Promise<User> // 「将来Userを返すPromise」という意味
Promise<string> // 「将来stringを返すPromise」
Promise<void> // 「将来何も返さない(完了だけ知らせる)Promise」
TypeScriptasync / awaitとは何か ― Promiseを「同期っぽく」書くための仕組み
async関数の役割
async を関数につけると、その関数は 必ずPromiseを返す関数 になります。
async function example(): Promise<number> {
return 42; // 実際には Promise<number> として返される
}
TypeScriptasyncを付けることで、 「この関数は非同期処理を含むかもしれないよ」という宣言になります。- 関数の戻り値の型は、
Promise<...>になります。
awaitの役割
await は、
「Promiseの結果が返ってくるまで待って、その中身を取り出す」
ためのキーワードです。
async function example(): Promise<void> {
const value = await someAsyncFunction(); // someAsyncFunctionはPromiseを返す
console.log(value); // Promiseの中身(解決された値)を使える
}
TypeScriptawaitはasync関数の中でしか使えない というルールがあります。awaitを使うことで、 「非同期処理の結果を、あたかも同期処理のように扱う」書き方ができます。
Step 1:User型を用意する
まずは、疑似APIで返したい User 型を定義します。
// User型の定義
type User = {
id: number;
name: string;
email: string;
};
TypeScriptシンプルに、ID・名前・メールアドレスだけを持つユーザーとします。
Step 2:疑似API関数 getUser を設計する
次に、疑似APIとして getUser 関数を作ります。
要件はこうです。
async function getUser(): Promise<User>という形にする- 内部で「APIからデータを取ってくるっぽい」処理をする
- 少し待ってから
Userを返す(擬似的な遅延)
setTimeoutで「遅延」を再現する
ブラウザやNode.jsでは、setTimeout を使って「一定時間後に処理を実行する」ことができます。 これを使って、疑似的なAPI遅延を再現します。
// 疑似API:ユーザー情報を取得する
async function getUser(): Promise<User> {
// Promiseを自分で作って、その中でsetTimeoutを使う
return new Promise<User>((resolve) => {
// 1秒後にユーザー情報を返す
setTimeout(() => {
const user: User = {
id: 1,
name: "Taro",
email: "[email protected]",
};
resolve(user); // 成功時の値を返す
}, 1000);
});
}
TypeScriptここが重要ポイントです:
new Promise<User>((resolve) => { ... })- 「将来Userを返すPromise」を自分で作っています。
setTimeout(() => { ... }, 1000);- 1秒後に
resolve(user)を呼び出すことで、 「1秒待ってからUserが手に入る」Promiseになっています。
- 1秒後に
async function getUser(): Promise<User>asyncを付けることで、戻り値の型がPromise<User>であることを明示しています。
Step 3:getUserを呼び出してみる(await版)
次に、この getUser を実際に呼び出してみます。 await を使うと、Promiseの結果を「普通の値」のように扱えます。
// getUserを使ってユーザー情報を取得し、表示する関数
async function showUser(): Promise<void> {
console.log("ユーザー情報を取得中...");
// getUserの結果が返ってくるまで待つ
const user = await getUser();
console.log("取得したユーザー:");
console.log(`ID: ${user.id}`);
console.log(`名前: ${user.name}`);
console.log(`メール: ${user.email}`);
}
// 実行例
showUser();
TypeScriptポイント:
await getUser();getUser()はPromise<User>を返しますが、awaitを使うことで「中身のUser」を取り出しています。
showUser自体もasync関数なので、戻り値はPromise<void>になります。- 「非同期処理を含む関数は、基本的にPromiseを返す」と覚えておくとよいです。
Step 4:エラー処理 ― try / catch で失敗に備える
現実のAPI通信では、常に成功するとは限りません。
- ネットワークエラー
- サーバーエラー
- タイムアウト
など、いろいろな失敗が起こり得ます。 そのため、非同期処理では エラー処理 がとても重要です。
疑似APIに「失敗の可能性」を追加する
まず、getUser を少し改造して、 たまに失敗する疑似APIにしてみます。
// 疑似API:たまに失敗するバージョン
async function getUserWithError(): Promise<User> {
return new Promise<User>((resolve, reject) => {
setTimeout(() => {
const shouldFail = Math.random() < 0.3; // 30%の確率で失敗
if (shouldFail) {
// 失敗時はrejectを呼ぶ
reject(new Error("ユーザー情報の取得に失敗しました"));
return;
}
// 成功時はresolveを呼ぶ
const user: User = {
id: 1,
name: "Taro",
email: "[email protected]",
};
resolve(user);
}, 1000);
});
}
TypeScriptここが重要ポイントです:
new Promise<User>((resolve, reject) => { ... })- 成功時は
resolve(value)、失敗時はreject(error)を呼びます。
- 成功時は
reject(new Error("..."))- これにより、Promiseは「失敗状態」として完了します。
awaitで待っている側では、例外として扱われます。
try / catchでエラーを受け止める
次に、この失敗する可能性のあるAPIを呼び出す側で、 try / catch を使ってエラーを受け止めます。
// エラーを考慮したユーザー表示関数
async function showUserSafely(): Promise<void> {
console.log("ユーザー情報を取得中...");
try {
const user = await getUserWithError(); // ここで失敗する可能性がある
console.log("取得したユーザー:");
console.log(`ID: ${user.id}`);
console.log(`名前: ${user.name}`);
console.log(`メール: ${user.email}`);
} catch (error) {
// 失敗時はここに入ってくる
console.error("ユーザー情報の取得に失敗しました");
console.error("詳細:", (error as Error).message);
}
}
// 実行例
showUserSafely();
TypeScript重要ポイント:
try { ... } catch (error) { ... }awaitしている非同期処理がrejectされた場合、 そのエラーはcatchブロックで受け止められます。
(error as Error).message- TypeScriptでは、
errorの型がunknownになることが多いので、as Errorでキャストしてからmessageを参照しています。
- TypeScriptでは、
Step 5:Promiseの型を意識して書いてみる
TypeScriptでは、Promiseの型を明示することで、 「この非同期関数が何を返すのか」をはっきりさせることができます。
基本パターン
async function foo(): Promise<number> { ... }- 将来
numberを返す非同期関数
- 将来
async function bar(): Promise<void> { ... }- 将来「完了だけ」を知らせる非同期関数
async function baz(): Promise<User[]> { ... }- 将来
Userの配列を返す非同期関数
- 将来
getUserの型を改めて確認する
async function getUser(): Promise<User> {
// ...
}
TypeScript- 戻り値の型が
Promise<User>であることを明示することで、 呼び出し側は「この関数は非同期でUserを返す」と理解できます。 await getUser()と書いたとき、userの型はUserになります(Promiseの中身の型)。
まとめ用テンプレート:Day 28の疑似API一式
最後に、Day 28の内容をまとめた練習用テンプレートを載せます。 このままコピーして、エラー率を変えたり、 User にプロパティを追加したりしながら、 非同期処理の感覚を育ててみてください。
// day28-async-api.ts
// Day 28:非同期処理・疑似API練習
// User型
type User = {
id: number;
name: string;
email: string;
};
// 成功する疑似API
async function getUser(): Promise<User> {
return new Promise<User>((resolve) => {
setTimeout(() => {
const user: User = {
id: 1,
name: "Taro",
email: "[email protected]",
};
resolve(user);
}, 1000); // 1秒後に成功
});
}
// たまに失敗する疑似API
async function getUserWithError(): Promise<User> {
return new Promise<User>((resolve, reject) => {
setTimeout(() => {
const shouldFail = Math.random() < 0.3; // 30%の確率で失敗
if (shouldFail) {
reject(new Error("ユーザー情報の取得に失敗しました"));
return;
}
const user: User = {
id: 1,
name: "Taro",
email: "[email protected]",
};
resolve(user);
}, 1000);
});
}
// シンプルな表示関数(成功前提)
async function showUser(): Promise<void> {
console.log("=== showUser ===");
console.log("ユーザー情報を取得中...");
const user = await getUser();
console.log("取得したユーザー:");
console.log(`ID: ${user.id}`);
console.log(`名前: ${user.name}`);
console.log(`メール: ${user.email}`);
}
// エラー対応版表示関数
async function showUserSafely(): Promise<void> {
console.log("=== showUserSafely ===");
console.log("ユーザー情報を取得中...");
try {
const user = await getUserWithError();
console.log("取得したユーザー:");
console.log(`ID: ${user.id}`);
console.log(`名前: ${user.name}`);
console.log(`メール: ${user.email}`);
} catch (error) {
console.error("ユーザー情報の取得に失敗しました");
console.error("詳細:", (error as Error).message);
}
}
// 実行例
showUser().then(() => {
// showUserが終わったら次を実行
return showUserSafely();
});
TypeScriptDay 28のまとめ
Day 28では、
- 非同期処理のイメージ(「すぐ終わらない処理」との付き合い方)
- Promiseとは何か(「未来の値の約束」)
async/awaitを使って、Promiseを「同期っぽく」扱う書き方try/catchで非同期処理のエラーを受け止める方法async function getUser(): Promise<User>という形で、 疑似APIを設計・実装する流れ
を、具体的なコードとコメント付きで見てきました。
非同期処理は、最初は少しとっつきにくく感じるかもしれませんが、 今日のように「疑似API」を題材にして何度か書いてみると、 だんだんと「時間の流れ」を意識したコードが自然に書けるようになっていきます。
次のステップとして、
getUserに「ユーザーIDを引数で渡す」ようにするgetUsers(): Promise<User[]>のように、配列を返すAPIを作る- 複数のAPIを
awaitで順番に呼び出す
などを試してみると、 非同期処理の世界がさらに立体的に見えてくるはずです。
Day 28:非同期処理演習 ― 成功・エラー・Loadingを全部扱ってみる
Day 28の演習テーマは、
非同期処理 成功処理 エラー処理 Loading状態 を実装する。
というものです。 ここまで学んだ Promise / async / await / try / catch / Promiseの型 を使って、 「API通信の基本パターン」を小さなサンプルで体験していきます。
ここでは、ユーザー情報を取得する疑似APIを題材にして、
- 非同期でデータを取りに行く
- その間は「Loading状態」
- 成功したら「成功状態」
- 失敗したら「エラー状態」
という、よくある流れをステップバイステップでかみ砕いていきます。
状態を3つに分けて考える:loading / success / error
まず、非同期処理のときに意識したい状態は、ざっくり次の3つです。
- Loading状態:
- 「今まさにデータを取りに行っているところ」
- 画面では「読み込み中…」などを表示するイメージです。
- Success状態:
- 「データ取得に成功して、結果が手元にある状態」
- 画面では、取得したユーザー情報などを表示します。
- Error状態:
- 「何かの理由でデータ取得に失敗した状態」
- 画面では、「エラーが発生しました」などのメッセージを表示します。
この3つをコードの中でどう表現するか、というのが今回の演習のポイントです。
Step 1:User型と状態管理用の型を用意する
まずは、ユーザー情報と状態を表す型を定義します。
// ユーザー情報の型
type User = {
id: number;
name: string;
email: string;
};
// 非同期処理の状態を表す型
type AsyncState<T> = {
loading: boolean; // 読み込み中かどうか
data: T | null; // 成功時のデータ(まだない場合はnull)
error: string | null; // エラーメッセージ(エラーがない場合はnull)
};
TypeScriptポイント:
AsyncState<T>はジェネリック型で、 「どんなデータ型でも同じパターンで状態管理できる」ようにしています。loading/data/errorの3つを持つことで、 成功・失敗・読み込み中をひとまとめに扱えます。
Step 2:疑似APIを作る ― 成功も失敗もあり得る
次に、ユーザー情報を取得する疑似APIを作ります。 ここでは、たまに失敗するようにして、エラー処理も試せるようにします。
// 疑似API:ユーザー情報を取得する(たまに失敗する)
async function fetchUser(): Promise<User> {
return new Promise<User>((resolve, reject) => {
console.log("API呼び出し開始...");
setTimeout(() => {
const shouldFail = Math.random() < 0.3; // 30%の確率で失敗
if (shouldFail) {
// 失敗時はrejectを呼ぶ
reject(new Error("ユーザー情報の取得に失敗しました"));
return;
}
// 成功時はresolveを呼ぶ
const user: User = {
id: 1,
name: "Taro",
email: "[email protected]",
};
resolve(user);
}, 1000); // 1秒後に結果を返す
});
}
TypeScript重要ポイント:
Promise<User>を自分で作り、成功時はresolve(user)、失敗時はreject(error)を呼んでいます。async function fetchUser(): Promise<User>とすることで、 「この関数は非同期でUserを返す」ということが型からも分かります。
Step 3:状態を管理しながら非同期処理を実行する関数を書く
次に、AsyncState<User> を使って、
- Loading状態にする
- 成功したらdataにUserを入れる
- 失敗したらerrorにメッセージを入れる
という流れをひとつの関数にまとめます。
// ユーザー取得の状態を管理する関数
async function loadUser(): Promise<AsyncState<User>> {
// まずはLoading状態からスタート
let state: AsyncState<User> = {
loading: true,
data: null,
error: null,
};
console.log("状態: loading =", state.loading);
try {
// 非同期API呼び出し(ここで成功・失敗が決まる)
const user = await fetchUser();
// 成功したら、loadingをfalseにしてdataにユーザーをセット
state = {
loading: false,
data: user,
error: null,
};
console.log("状態更新: 成功");
return state;
} catch (error) {
// 失敗したら、loadingをfalseにしてerrorにメッセージをセット
state = {
loading: false,
data: null,
error: (error as Error).message,
};
console.log("状態更新: エラー");
return state;
}
}
TypeScriptここが今回の演習の核心です:
loadingを最初にtrueにして、「今読み込み中だよ」と明示しています。await fetchUser()が成功したら、dataにユーザー情報を入れ、errorはnullにします。- 失敗したら、
errorにメッセージを入れ、dataはnullのままにします。 - どちらの場合も、最後は
loading: falseにして、「読み込みは終わった」状態にします。
Step 4:状態に応じて表示を切り替える
次に、loadUser() の結果を使って、 状態に応じて表示を切り替える処理を書いてみます。
// 状態に応じて表示を切り替える関数
async function showUserWithState(): Promise<void> {
console.log("=== ユーザー情報取得処理開始 ===");
// 非同期処理+状態管理
const state = await loadUser();
console.log("=== 結果表示 ===");
if (state.loading) {
// 本来ここには来ない想定ですが、念のため
console.log("読み込み中です...");
return;
}
if (state.error) {
// エラー状態
console.log("エラーが発生しました:");
console.log(state.error);
return;
}
if (state.data) {
// 成功状態
console.log("ユーザー情報の取得に成功しました:");
console.log(`ID: ${state.data.id}`);
console.log(`名前: ${state.data.name}`);
console.log(`メール: ${state.data.email}`);
return;
}
// ここに来るのはかなり例外的ですが、念のため
console.log("予期しない状態です(dataもerrorもありません)");
}
// 実行例
showUserWithState();
TypeScriptポイント:
state.loading/state.error/state.dataを順番にチェックすることで、 「今どの状態なのか」を分岐しています。- 実際の画面(UI)では、
loading === true→ スピナーや「読み込み中…」表示error !== null→ エラーメッセージ表示data !== null→ 実データ表示 という形で使われることが多いです。
Step 5:Loading状態をもう少し意識してみる
コンソールだけだと少し分かりにくいので、 Loading状態をログで強調してみます。
async function showUserWithLoadingLog(): Promise<void> {
console.log("=== ユーザー情報取得(Loading状態付き) ===");
// Loading開始
console.log("Loading... ユーザー情報を取得しています。");
try {
const user = await fetchUser(); // ここで1秒ほど待つ
console.log("Loading完了(成功)");
console.log("取得したユーザー:");
console.log(`ID: ${user.id}`);
console.log(`名前: ${user.name}`);
console.log(`メール: ${user.email}`);
} catch (error) {
console.log("Loading完了(エラー)");
console.error("ユーザー情報の取得に失敗しました");
console.error("詳細:", (error as Error).message);
}
}
// 実行例
showUserWithLoadingLog();
TypeScriptここで意識したいこと:
- 「Loading中」と「Loading完了(成功/エラー)」をログで分けて出すことで、 非同期処理の時間の流れがイメージしやすくなります。
- UIでは、この「Loading中」のタイミングでスピナーを表示し、 完了したらスピナーを消して結果を表示する、という形になります。
まとめ用テンプレート:Day 28演習一式
最後に、今回の演習内容をひとつのファイルにまとめたテンプレートを載せます。 このままコピーして、エラー率を変えたり、 User にプロパティを追加したりしながら、 非同期処理+状態管理の感覚を育ててみてください。
// day28-async-state.ts
// Day 28:非同期処理演習(成功・エラー・Loading)
// ユーザー情報の型
type User = {
id: number;
name: string;
email: string;
};
// 非同期処理の状態を表すジェネリック型
type AsyncState<T> = {
loading: boolean;
data: T | null;
error: string | null;
};
// 疑似API:ユーザー情報を取得する(たまに失敗する)
async function fetchUser(): Promise<User> {
return new Promise<User>((resolve, reject) => {
console.log("API呼び出し開始...");
setTimeout(() => {
const shouldFail = Math.random() < 0.3; // 30%の確率で失敗
if (shouldFail) {
reject(new Error("ユーザー情報の取得に失敗しました"));
return;
}
const user: User = {
id: 1,
name: "Taro",
email: "[email protected]",
};
resolve(user);
}, 1000);
});
}
// 状態管理付きのユーザー取得関数
async function loadUser(): Promise<AsyncState<User>> {
let state: AsyncState<User> = {
loading: true,
data: null,
error: null,
};
console.log("状態: loading =", state.loading);
try {
const user = await fetchUser();
state = {
loading: false,
data: user,
error: null,
};
console.log("状態更新: 成功");
return state;
} catch (error) {
state = {
loading: false,
data: null,
error: (error as Error).message,
};
console.log("状態更新: エラー");
return state;
}
}
// 状態に応じて表示を切り替える関数
async function showUserWithState(): Promise<void> {
console.log("=== ユーザー情報取得処理開始 ===");
const state = await loadUser();
console.log("=== 結果表示 ===");
if (state.loading) {
console.log("読み込み中です...");
return;
}
if (state.error) {
console.log("エラーが発生しました:");
console.log(state.error);
return;
}
if (state.data) {
console.log("ユーザー情報の取得に成功しました:");
console.log(`ID: ${state.data.id}`);
console.log(`名前: ${state.data.name}`);
console.log(`メール: ${state.data.email}`);
return;
}
console.log("予期しない状態です(dataもerrorもありません)");
}
// Loadingログ付きの表示関数
async function showUserWithLoadingLog(): Promise<void> {
console.log("=== ユーザー情報取得(Loading状態付き) ===");
console.log("Loading... ユーザー情報を取得しています。");
try {
const user = await fetchUser();
console.log("Loading完了(成功)");
console.log("取得したユーザー:");
console.log(`ID: ${user.id}`);
console.log(`名前: ${user.name}`);
console.log(`メール: ${user.email}`);
} catch (error) {
console.log("Loading完了(エラー)");
console.error("ユーザー情報の取得に失敗しました");
console.error("詳細:", (error as Error).message);
}
}
// 実行例
showUserWithState().then(() => {
return showUserWithLoadingLog();
});
TypeScriptDay 28演習のまとめ
今回の演習では、
- 非同期処理の3つの状態(Loading / Success / Error)を意識した設計
Promise<T>として非同期関数の戻り値の型を明示することasync/awaitを使って、非同期処理を「読みやすい形」で書く方法try/catchでエラーを受け止め、状態に反映する流れ
を、疑似APIと状態管理のコードを通して体験しました。
実際のフロントエンド開発では、 この「Loading・成功・エラー」の3状態をどう扱うかが、 ユーザー体験の質を大きく左右します。
今日のテンプレートをベースに、
AsyncState<T>を別ファイルに切り出して再利用するfetchUserをfetchUsers(配列)にして一覧表示を試す- エラー時にリトライボタンを出すイメージで処理を追加する
など、少しずつ発展させていくと、 非同期処理と状態管理の感覚がぐっと深まっていくはずです。
