「プロとしての非同期設計」って何が違うのか
非同期処理を「書ける」ようになるのは、正直そんなに難しくありません。async/await を覚えて、fetch を呼んで、try/catch を付ければ、とりあえず動くものは作れます。
でも、プロとして非同期を「設計する」となると、見るべきものが一気に増えます。
読みやすさ、テストしやすさ、エラーの扱い、UI との境界、パフォーマンス、将来の変更余地…。
ここで話したいのは、
「プロの非同期設計って、何を意識しているのか」
「初心者がどこからその感覚に近づいていけるのか」
というところです。
コードのテクニックというより、
非同期をどう“考えるか”にフォーカスして話していきます。
プロの非同期設計は「呼び出し側」から逆算する
まず「どう呼ばれたいか」を決める
プロっぽい非同期設計の一番の特徴は、
「中身から書き始めない」ことです。
いきなりこう書くのではなく。
async function fetchUser() {
const res = await fetch("/api/user");
return res.json();
}
JavaScript先に「呼び出し側の理想の形」を考えます。
例えば、ユーザーページを作るなら、こう書けたら気持ちいいな、と想像します。
const { user, posts } = await loadUserPageData(userId);
renderUserPage(user, posts);
JavaScriptこの「理想の一行」を先に決めてから、
それを実現するために中身の非同期処理を設計していきます。
例えば、こう分解されていきます。
async function fetchUser(userId) { ... }
async function fetchUserPosts(userId) { ... }
async function loadUserPageData(userId) {
const [user, posts] = await Promise.all([
fetchUser(userId),
fetchUserPosts(userId),
]);
return { user, posts };
}
JavaScriptここで重要なのは、
「非同期関数の設計を、“使う側のコード”から逆算している」
ということです。
プロの非同期設計は、
「この関数を使う人が、どう書けたら楽か?」
を常に意識しています。
「一文で説明できる責務」にする
もう一つのポイントは、
「この非同期関数は、一言で何をする関数か?」
をはっきりさせることです。
例えば、
fetchUser は「ユーザーを取得する」fetchUserPosts は「ユーザーの投稿を取得する」loadUserPageData は「ユーザーページに必要なデータをまとめて取得する」
このくらいスパッと説明できると、
関数の責務がぶれていません。
逆に、こういう関数名と中身は危険です。
async function doInit() {
const res = await fetch("/api/user");
const user = await res.json();
const postsRes = await fetch(`/api/posts?userId=${user.id}`);
const posts = await postsRes.json();
renderUser(user);
renderPosts(posts);
saveToCache(user, posts);
}
JavaScript「何をする関数?」と聞かれても、
「いろいろやる関数」としか答えられません。
プロとしての非同期設計では、
「一つの関数に“いろいろ”を詰め込まない」
という感覚がかなり強いです。
プロは「失敗の仕方」まで設計している
成功だけでなく「どう壊れるか」も仕様
非同期処理は、失敗が前提です。
ネットワークは落ちるし、サーバーはエラーを返すし、JSON は壊れます。
プロの非同期設計は、
「成功したときに何を返すか」だけでなく、
「失敗したときにどう振る舞うか」まで仕様として決めます。
例えば、こういう関数があるとします。
async function fetchCurrentUser() {
const res = await fetch("/api/user");
if (!res.ok) {
throw new Error("ユーザー取得に失敗しました");
}
return res.json();
}
JavaScriptここで終わらせず、
頭の中ではこう整理しています。
この関数は、ネットワークエラーのときは何を投げるか
HTTP ステータスが 401, 403, 500 のときはどう扱うか
JSON パースに失敗したらどうなるか
そして、それをコメントやドキュメントとして残します。
/**
* 現在のユーザーを取得する。
*
* 成功時:
* - ユーザーオブジェクトを解決する Promise を返す。
*
* 失敗時:
* - ネットワークエラー: fetch が投げる TypeError をそのまま伝播。
* - HTTP ステータスが 2xx 以外: Error を投げる(メッセージにステータスコードを含む)。
* - JSON パースエラー: SyntaxError をそのまま伝播。
*/
JavaScriptここがプロっぽいポイントです。
「例外は“たまたま起きるもの”ではなく、“仕様として設計するもの”」
という感覚を持っています。
「投げるか」「返すか」を一貫させる
もう一つ大事なのが、
「この API は失敗を例外で表現するのか、戻り値で表現するのか」
を決めて、一貫させることです。
例えば、こういうスタイル。
async function fetchUserSafe(id) {
try {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) {
return { ok: false, error: new Error("ユーザー取得に失敗しました") };
}
const user = await res.json();
return { ok: true, user };
} catch (err) {
return { ok: false, error: err };
}
}
JavaScriptこの関数は「例外を外に投げない」という仕様です。
呼び出し側は、常に ok を見ればよい。
プロの設計では、
「このプロジェクトでは、外向きの API はこのスタイルで統一しよう」
というルールを決めて、ブレないようにします。
初心者がやりがちなのは、
ある関数は例外を投げ、別の関数は { ok: false } を返し、
呼び出し側が毎回「これはどっちだっけ?」と迷う状態です。
プロはそこを「迷わせない」ように設計します。
プロは「テストしやすさ」を最初から組み込む
外部依存を中に埋め込まない
非同期処理は、テストが難しくなりがちです。
ネットワーク、時間、ストレージなど、外部要因が多いからです。
プロの非同期設計は、
「テストのときに差し替えられるように書く」
ことを最初から意識しています。
例えば、こうではなく。
async function fetchCurrentUser() {
const res = await fetch("/api/user");
return res.json();
}
JavaScriptこうします。
async function fetchCurrentUser(fetchImpl) {
const res = await fetchImpl("/api/user");
return res.json();
}
JavaScript本番では fetch を渡し、
テストではフェイクの fetch を渡せます。
async function fakeFetch() {
return {
ok: true,
json: async () => ({ id: 1, name: "テスト太郎" }),
};
}
JavaScriptこれで、
「ネットワークに依存しないテスト」が簡単に書けます。
プロは、
「テストのために後からモックをねじ込む」のではなく、
「最初からモックしやすい形に設計する」
という発想で非同期関数を書きます。
ロジックと副作用を分ける
もう一つのテストしやすさのポイントは、
「ロジック」と「外部とのやり取り」を分けることです。
例えば、こういう関数があったとします。
async function initPage() {
const res = await fetch("/api/user");
const user = await res.json();
const header = document.querySelector("#header");
header.textContent = `こんにちは、${user.name}さん`;
}
JavaScriptこれをテストしようとすると、
fetch も DOM も全部相手にしないといけません。
プロは、こう分解します。
async function fetchCurrentUser(fetchImpl) {
const res = await fetchImpl("/api/user");
return res.json();
}
function formatGreeting(user) {
return `こんにちは、${user.name}さん`;
}
function renderHeader(element, text) {
element.textContent = text;
}
JavaScriptこれで、formatGreeting は完全に同期で、外部依存もなく、テストが超簡単です。
非同期の中にも「ただのロジック」が必ずあります。
プロはそこを見つけて、切り出して、テストしやすくします。
プロは「時間」と「順番」を設計として扱う
レースコンディションを“運”に任せない
非同期バグの多くは、
「どっちが先に終わるか分からない」
「キャンセルしたはずの処理が後から結果を反映する」
といった、時間と順番の問題です。
プロの非同期設計は、
そこを「運任せ」にしません。
例えば、検索ボックスの例。
素直に書くと、こうなります。
input.addEventListener("input", async () => {
const keyword = input.value;
const res = await fetch(`/api/search?q=${encodeURIComponent(keyword)}`);
const data = await res.json();
renderResult(data);
});
JavaScriptこれだと、
「古いリクエストの結果が後から返ってきて、最新の結果を上書きする」
というバグが起きます。
プロは、こういうガードを入れます。
let latestRequestId = 0;
input.addEventListener("input", async () => {
const keyword = input.value;
const id = ++latestRequestId;
const res = await fetch(`/api/search?q=${encodeURIComponent(keyword)}`);
const data = await res.json();
if (id !== latestRequestId) {
return; // 古いリクエストなので無視
}
renderResult(data);
});
JavaScriptここでやっているのは、
「時間と順番を、ID という形でコードに持ち込んでいる」
ということです。
プロは、
「非同期は順番が保証されない」
という前提を受け入れたうえで、
「それでも壊れないルール」をコードに埋め込みます。
キャンセル可能性を設計に入れる
もう一つ、プロがよく意識するのが「キャンセル」です。
例えば、画面 A で API を叩いている途中に、
ユーザーが画面 B に移動したとします。
そのとき、
「画面 A 用のリクエスト結果が、後から UI に反映されてしまう」
というバグがよく起きます。
プロは、AbortController などを使って「キャンセル可能な API」を設計します。
async function fetchUser(id, { signal } = {}) {
const res = await fetch(`/api/users/${id}`, { signal });
if (!res.ok) throw new Error("ユーザー取得に失敗しました");
return res.json();
}
JavaScript画面側では、こう扱います。
const controller = new AbortController();
async function loadUserPage(id) {
try {
const user = await fetchUser(id, { signal: controller.signal });
renderUser(user);
} catch (err) {
if (err.name === "AbortError") {
return; // キャンセルされた場合は何もしない
}
showError(err.message);
}
}
// 画面を離れるとき
controller.abort();
JavaScriptプロの非同期設計は、
「ユーザーが途中でやめる」「画面が変わる」
といった現実の動きを前提にしています。
プロは「境界」をはっきり引く
同期と非同期の境界
プロは、
「どこから先が非同期の世界か」
「どこまでが同期の世界か」
を意識して境界を引きます。
例えば、こういう構造を作ります。
データ取得層(非同期)
状態管理層(同期)
UI 層(同期)
コードで言うと、こんなイメージです。
async function fetchUser(id) { ... } // 非同期
async function fetchPosts(userId) { ... } // 非同期
function createUserStore() { // 同期
let state = { user: null, posts: [] };
return {
setUser(user) { state.user = user; },
setPosts(posts) { state.posts = posts; },
getState() { return state; },
};
}
function renderUserPage(state) { // 同期
// DOM 更新
}
JavaScriptそして、これらを組み合わせる「オーケストレーション層」を非同期で書きます。
async function loadUserPage(id, { store }) {
const [user, posts] = await Promise.all([
fetchUser(id),
fetchPosts(id),
]);
store.setUser(user);
store.setPosts(posts);
renderUserPage(store.getState());
}
JavaScriptプロは、
「非同期の世界」と「同期の世界」をごちゃ混ぜにせず、
レイヤーごとに責務を分けます。
これによって、
テストしやすさ、変更しやすさ、見通しの良さが一気に上がります。
初心者が「プロとしての非同期設計」に近づくための視点
最後に、あなたに持っていてほしい問いをまとめます。
これを自分のコードに何度も投げかけてみてください。
この非同期関数は、「どう呼ばれたいか」から逆算して設計されているか。
この関数は、一文で「何をする関数」と説明できるか。
成功だけでなく、「どう失敗しうるか」を言葉にできるか。
外部依存(fetch、DOM、時間など)を中に埋め込まず、差し替えられる形にできているか。
時間と順番の問題(レースコンディション、キャンセル)を、ルールとしてコードに落とし込めているか。
同期と非同期の境界、UI とデータ取得の境界が、コード上で見える形になっているか。
おすすめの練習は、
自分の非同期処理を一つ選んで、こうしてみることです。
まず「理想の呼び出し側のコード」を書く。
次に「その理想を実現するために、関数をどう分けるか」を考える。
最後に「失敗の仕方」「テストのしやすさ」「時間と順番」をチェックする。
それを何度か繰り返すと、
あなたの中に「プロとしての非同期設計の感覚」が、確実に育っていきます。
async/await を知っているだけの人と、
非同期を“設計できる”人の差は、そこにあります。

