なぜ「同期 API との境界」を意識しないといけないのか
非同期処理を学び始めると、
「どこまでを同期で書いて、どこからを非同期にするのか」
がだんだん分からなくなってきます。
全部 async にしてしまうのも違うし、
逆に何でも同期でやろうとすると UI が固まる。
ここで大事になるのが、
「同期 API と非同期 API の境界を、意識して設計する」
という考え方です。
境界をちゃんと決めておくと、
どの関数はすぐ結果が返るのか
どの関数は await が必要なのか
どこから先は「待ち」が発生する世界なのか
が、コードから読み取りやすくなります。
そもそも「同期 API」と「非同期 API」の違い
同期 API:呼んだらすぐ終わる世界
同期 API は、呼び出した瞬間に処理が始まり、
終わるまで呼び出し元に制御が戻ってきません。
例えば、配列の長さを取るのは完全に同期です。
function getLength(arr) {
return arr.length;
}
const len = getLength([1, 2, 3]); // すぐに 3 が返る
JavaScriptここには「待ち時間」という概念はありません。
CPU が計算して、すぐ結果が返ってきます。
非同期 API:呼んだら「あとで結果が返ってくる」世界
非同期 API は、呼んだ瞬間には結果が手に入らず、
Promise を返して「あとで結果を教えるね」というスタイルになります。
async function fetchUser(id) {
const res = await fetch(`/api/users/${id}`);
return res.json();
}
const userPromise = fetchUser(1); // ここではまだ結果はない
const user = await userPromise; // ここで初めて結果が手に入る
JavaScriptここには「待ち」があります。
ネットワークやディスクなど、時間のかかるものが絡みます。
重要なのは、
「同期か非同期か」は“実装の都合”ではなく、“呼び出し側の体験”に直結する仕様
だということです。
どこから非同期にするか?境界の考え方
「時間がかかる可能性があるところ」から先を非同期にする
基本的な考え方はシンプルです。
メモリ上の値をちょっと計算するだけ
配列をフィルタするだけ
オブジェクトを組み立てるだけ
こういうのは同期で十分です。
一方で、
ネットワーク通信(fetch)
ディスク I/O(IndexedDB など)
タイマー(setTimeout, setInterval)
重い計算(長時間のループなど)
こういう「時間が読めないもの」「長くなる可能性があるもの」は、
非同期の世界に追い出した方が安全です。
つまり、
「この処理は、ユーザーを待たせる可能性があるか?」
という視点で境界を決めていきます。
境界をまたぐ関数は「どちら側の顔」を持つかを決める
例えば、キャッシュとネットワークを組み合わせる関数を考えます。
キャッシュにあればすぐ返したい(同期っぽい)
なければネットワークから取ってきたい(非同期)
このとき、API をどう設計するか。
選択肢は二つあります。
一つ目は、「常に非同期 API として見せる」パターン。
async function getUser(id) {
const cached = userCache.get(id);
if (cached) {
return cached; // ここは実質同期だが、async 関数なので Promise で返る
}
const res = await fetch(`/api/users/${id}`);
const user = await res.json();
userCache.set(id, user);
return user;
}
JavaScript呼び出し側は、常に await します。
const user = await getUser(1);
JavaScript二つ目は、「同期 API と非同期 API を分ける」パターン。
function getUserFromCache(id) {
return userCache.get(id) ?? null;
}
async function fetchUserFromServer(id) {
const res = await fetch(`/api/users/${id}`);
return res.json();
}
JavaScript呼び出し側は、こう書きます。
let user = getUserFromCache(1);
if (!user) {
user = await fetchUserFromServer(1);
}
JavaScriptどちらが正解というより、
「この関数は“非同期の顔”で見せるのか、“同期の顔”と“非同期の顔”を分けるのか」
を意識して決めることが大事です。
ラッパーで「境界」をはっきりさせる
同期の世界を包んで非同期 API にする
ときどき、「中身は同期だけど、外からは非同期として扱いたい」
というケースがあります。
例えば、将来ネットワークアクセスに変わるかもしれない処理。
function getConfigSync() {
return {
theme: "dark",
language: "ja",
};
}
JavaScript今は同期で十分ですが、
将来サーバーから設定を取ってくるように変わるかもしれません。
そのときに備えて、最初から「非同期 API の顔」を用意しておく、という設計もあります。
async function getConfig() {
return getConfigSync();
}
JavaScript呼び出し側は、最初からこう書きます。
const config = await getConfig();
JavaScript中身が同期か非同期かは、呼び出し側には関係ありません。
境界の外から見たときは、
「設定は await して手に入るもの」
という仕様だけが大事です。
ここが重要です。
「境界の外から見たときの“顔”を先に決めておくと、内部の実装を変えても呼び出し側を壊さずに済む。」
非同期の世界を包んで同期 API に見せるのは基本的に無理
逆に、「非同期処理を同期 API のように見せたい」という欲望も出てきます。
例えば、こういうのはできません。
// これはダメな例
function getUserSync(id) {
const res = await fetch(`/api/users/${id}`); // ここで await は使えない
return res.json();
}
JavaScriptJavaScript では、
「本当に時間のかかる処理」を完全に同期 API にすることは基本的にできません。
やろうとすると、
UI をブロックする危険なコードになります。
function blockFor(ms) {
const start = Date.now();
while (Date.now() - start < ms) {
// ひたすら待つ(UI が固まる)
}
}
JavaScriptだから、
「非同期を同期に“偽装”する」のではなく、
“非同期であることを正直に API に出す”
というのが健全な設計です。
UI から見た「同期と非同期の境界」
UI ロジックは「非同期の世界の手前」で止める
例えば、ボタンを押したらユーザー情報を読み込んで表示する UI を考えます。
何も考えずに書くと、こうなりがちです。
button.addEventListener("click", async () => {
const res = await fetch("/api/user");
const user = await res.json();
header.textContent = `こんにちは、${user.name}さん`;
});
JavaScriptこれでも動きますが、
UI と非同期処理がべったりくっついています。
境界を意識すると、こう分けられます。
async function fetchCurrentUser() {
const res = await fetch("/api/user");
if (!res.ok) throw new Error("ユーザー取得に失敗しました");
return res.json();
}
function renderUserHeader(user) {
header.textContent = `こんにちは、${user.name}さん`;
}
button.addEventListener("click", async () => {
try {
const user = await fetchCurrentUser();
renderUserHeader(user);
} catch (err) {
showErrorMessage(err.message);
}
});
JavaScriptここでの境界は、
fetchCurrentUser より内側 → 非同期の世界(ネットワーク)renderUserHeader より外側 → UI の世界(同期)
です。
UI 側から見たとき、
「非同期の世界に入るのはここから」
という線が見えるようにしておくと、
コードの見通しがかなり良くなります。
初心者として「同期 API との境界」で意識してほしいこと
最後に、頭の中に置いておいてほしい問いをまとめます。
この処理は、本当に同期でやっても大丈夫な軽さか?
この関数は、呼び出し側から見て“同期の顔”か“非同期の顔”か、はっきりしているか?
キャッシュとネットワークが混ざるところで、境界の設計をサボっていないか?
UI のコードの中に、非同期の詳細(fetch や長い await)が入り込みすぎていないか?
「将来中身が非同期に変わっても、呼び出し側を壊さない顔つき」になっているか?
おすすめの練習は、
自分のコードの中から一つ関数を選んで、
これは「同期 API」として見せたいのか
それとも「非同期 API」として見せたいのか
を言語化してみることです。
そのうえで、
同期として見せたいなら、中で時間のかかることをしていないか?
非同期として見せたいなら、Promise を返すきれいな形になっているか?
をチェックしてみてください。
その「境界」を意識し始めた瞬間から、
あなたの非同期コードはただ動くだけのものから、
「設計されたコード」 に変わっていきます。
