なぜ「非同期コードの可読性」がそんなに大事なのか
非同期処理って、動かすだけなら意外とすぐ書けます。async 付けて、await fetch(...) して、console.log して終わり——みたいなやつ。
でも、そこから一歩進んで
「あとから読んでも分かるコード」
「チームメンバーが見ても迷子にならないコード」
にしようとすると、急に難易度が上がります。
非同期コードは、同期コードよりも
・処理の順番が頭の中で追いにくい
・エラーの流れが見えにくい
・「どこで待っているのか」がパッと分かりにくい
という性質を持っています。
だからこそ、
「可読性を意識して非同期コードを書く」こと自体が、立派なスキル です。
ここでは、そのスキルを育てるための考え方と書き方を、例題付きで整理していきます。
可読性の土台:まず「同期っぽく読める形」に寄せる
async/await を「ちゃんと」使う
Promise チェーンは強力ですが、初心者にとっては読みづらくなりがちです。
例えば、こういうコード。
fetchUser()
.then((user) => {
return fetchPosts(user.id).then((posts) => {
return { user, posts };
});
})
.then((result) => {
console.log(result);
})
.catch((err) => {
console.error(err);
});
JavaScript動くし、間違ってはいません。
でも、ネストが深くなってくると、一気に読みにくくなります。
同じ処理を async/await で書くと、こうなります。
async function loadUserAndPosts() {
try {
const user = await fetchUser();
const posts = await fetchPosts(user.id);
return { user, posts };
} catch (err) {
console.error(err);
throw err;
}
}
loadUserAndPosts().then((result) => {
console.log(result);
});
JavaScript上から下に「順番に」読めますよね。
同期コードにかなり近い感覚で追えるはずです。
ここで重要なのは、
「非同期処理でも、読み手の頭の中では“順番に起きている物語”として読めるようにする」
という発想です。
async/await は、そのための強力な道具です。
Promise チェーンを全部捨てろ、ではなく、
「読みやすさを優先したいところは async/await に寄せる」
という感覚を持っておくといいです。
関数の分割:一つの関数に「物語を詰め込みすぎない」
「一画面分のストーリー」を一つの関数にする
可読性を上げるうえで、
非同期かどうかに関係なく効いてくるのが「関数の分割」です。
例えば、こんなコードがあったとします。
async function initPage() {
const userRes = await fetch("/api/user");
const user = await userRes.json();
const postsRes = await fetch(`/api/posts?userId=${user.id}`);
const posts = await postsRes.json();
const notificationsRes = await fetch(`/api/notifications?userId=${user.id}`);
const notifications = await notificationsRes.json();
renderHeader(user);
renderPosts(posts);
renderNotifications(notifications);
}
JavaScriptこれでもまだ読めますが、
「データ取得」と「描画」が一つの関数に混ざっています。
これを、役割ごとに分けてみます。
async function loadUserRelatedData() {
const userRes = await fetch("/api/user");
const user = await userRes.json();
const [postsRes, notificationsRes] = await Promise.all([
fetch(`/api/posts?userId=${user.id}`),
fetch(`/api/notifications?userId=${user.id}`),
]);
const [posts, notifications] = await Promise.all([
postsRes.json(),
notificationsRes.json(),
]);
return { user, posts, notifications };
}
function renderPage({ user, posts, notifications }) {
renderHeader(user);
renderPosts(posts);
renderNotifications(notifications);
}
async function initPage() {
const data = await loadUserRelatedData();
renderPage(data);
}
JavaScriptこうすると、
・loadUserRelatedData は「データを集める物語」
・renderPage は「画面を描く物語」
・initPage は「それらをつなぐ物語」
というふうに、役割がはっきり分かれます。
ここが重要です。
「一つの async 関数に、非同期も同期も全部詰め込まない。
“何をしている関数なのか”を一言で説明できる大きさに分割する。」
可読性は、関数の大きさと役割の明確さから始まります。
名前の付け方:非同期であることを「名前」で伝える
「何をするか」と「何を返すか」が分かる名前にする
非同期コードの可読性で、名前はかなり重要です。
例えば、次の二つを比べてみてください。
async function get() {
const res = await fetch("/api/user");
return res.json();
}
JavaScriptasync function fetchCurrentUser() {
const res = await fetch("/api/user");
return res.json();
}
JavaScript後者の方が、
「何をしている関数なのか」が一瞬で分かりますよね。
さらに、
「Promise を返すのか、値を返すのか」も名前で伝えられます。
function getUser() {
return fetch("/api/user").then((res) => res.json());
}
async function loadUser() {
const user = await getUser();
return user;
}
JavaScriptgetUser は「Promise を返す関数」loadUser は「実際に await して値を取ってくる関数」
という役割の違いを名前で表現しています。
ここが重要です。
「名前を見ただけで、“これは非同期っぽいな”“これは値が返ってきそうだな”と分かるようにする。」
初心者のうちは、fetchXxx, loadXxx, getXxxAsync など、
自分なりのルールを決めておくと、読みやすさが安定します。
エラー処理の見通しをよくする:try/catch の置き方
どこでエラーを「受け止めるか」をはっきりさせる
非同期コードの可読性を下げる大きな要因の一つが、
「エラーがどこで処理されているのか分からない」状態です。
例えば、こういうコード。
async function loadUser() {
const res = await fetch("/api/user");
return res.json();
}
async function init() {
const user = await loadUser();
renderUser(user);
}
JavaScriptこれだと、fetch が失敗したときにどうなるかが、
コードからは読み取りにくいです。
エラーを「ここで受け止める」と決めて、
try/catch を置いてあげると、ぐっと読みやすくなります。
async function loadUser() {
const res = await fetch("/api/user");
if (!res.ok) {
throw new Error("ユーザー取得に失敗しました");
}
return res.json();
}
async function init() {
try {
const user = await loadUser();
renderUser(user);
} catch (err) {
showErrorMessage(err.message);
}
}
JavaScriptこれなら、
・loadUser は「失敗したら例外を投げる関数」
・init は「その例外を受け止めて UI に反映する関数」
という役割がはっきりします。
ここが重要です。
「エラー処理の責任範囲を、関数ごとに決める。
“どこで投げて、どこで受け止めるか”がコードから読めるようにする。」
可読性の高い非同期コードは、
成功パスだけでなく失敗パスも「物語」として追えるようになっています。
「順番」と「並列」をコードで分かりやすく表現する
「これはあえて順番にやっている」をコードで示す
非同期コードのレビューでよくあるのが、
「これ並列にできるのに、なんで順番に await してるの?」
という指摘です。
でも、逆に
「これはあえて順番にやっている(順番が意味を持つ)」
というケースもあります。
例えば、こういうコード。
async function processInOrder(items) {
for (const item of items) {
await processItem(item);
}
}
JavaScriptこれを見たとき、
「並列にできるのでは?」と思う人もいるかもしれません。
でも、もし processItem が
「前の結果に依存する処理」だったり、
「順番通りにサーバーに送る必要がある処理」だったりしたら、
あえて直列にしている意味があります。
そういうときは、
コメントや名前で「意図」を書いておくと、可読性が一気に上がります。
async function processInOrderSequentially(items) {
// 順番が重要なので、あえて直列で処理する
for (const item of items) {
await processItem(item);
}
}
JavaScript逆に、「並列にしている」ことをはっきり書くのも大事です。
async function processInParallel(items) {
const promises = items.map((item) => processItem(item));
await Promise.all(promises);
}
JavaScriptここが重要です。
「順番にやるのか、並列にやるのか」を、
コードと名前とコメントで“意図として”表現する。
読み手が“推測”しなくていいようにする。
「読みやすい非同期コード」のミニ例題
悪い例と、少し良くした例を比べてみる
まず、ちょっと読みにくい例から。
async function init() {
const userRes = await fetch("/api/user");
const user = await userRes.json();
const postsRes = await fetch(`/api/posts?userId=${user.id}`);
const posts = await postsRes.json();
const notificationsRes = await fetch(`/api/notifications?userId=${user.id}`);
const notifications = await notificationsRes.json();
renderHeader(user);
renderPosts(posts);
renderNotifications(notifications);
}
JavaScript動くし、そこまでひどくはないですが、
・エラー処理がない
・データ取得と描画が混ざっている
・順番と依存関係がコードから読み取りにくい
という状態です。
これを「可読性」を意識して書き直すと、こうなります。
async function fetchCurrentUser() {
const res = await fetch("/api/user");
if (!res.ok) throw new Error("ユーザー取得に失敗しました");
return res.json();
}
async function fetchUserRelatedData(userId) {
const [postsRes, notificationsRes] = await Promise.all([
fetch(`/api/posts?userId=${userId}`),
fetch(`/api/notifications?userId=${userId}`),
]);
if (!postsRes.ok) throw new Error("投稿取得に失敗しました");
if (!notificationsRes.ok) throw new Error("通知取得に失敗しました");
const [posts, notifications] = await Promise.all([
postsRes.json(),
notificationsRes.json(),
]);
return { posts, notifications };
}
function renderPage({ user, posts, notifications }) {
renderHeader(user);
renderPosts(posts);
renderNotifications(notifications);
}
async function initPage() {
try {
const user = await fetchCurrentUser();
const { posts, notifications } = await fetchUserRelatedData(user.id);
renderPage({ user, posts, notifications });
} catch (err) {
showErrorMessage(err.message);
}
}
JavaScript長くはなりましたが、
・関数ごとに「何をしているか」がはっきりしている
・依存関係(ユーザー → そのユーザーに紐づくデータ)が見えやすい
・エラーの流れが追いやすい
・initPage を読むだけで「ページの物語」が分かる
という状態になっています。
ここが重要です。
「短いコード=読みやすい」ではない。
“物語として追えるかどうか”が、非同期コードの可読性の本質。
初心者として「非同期コードの可読性」で本当に意識してほしいこと
最後に、頭の中に置いておいてほしい問いをまとめます。
この async 関数は、一言で何をする関数と言えるか?
await の一つ一つに「ここで待つ理由」を説明できるか?
エラーが起きたときの流れを、上から下に追えるか?
順番にやるべきところと、並列にできるところがコードから分かるか?
関数名だけ見て、「これは非同期で何をして何を返すか」が想像できるか?
おすすめの練習は、
自分の書いた非同期コードを一つ選んで、
「同期コードだと思って物語として読んでみる」ことです。
途中で「ん?ここで何が起きてるんだ?」と引っかかった場所が、
可読性を上げるチャンスです。
そこに対して、
・関数を分ける
・名前を変える
・await の位置を整理する
・コメントで意図を書く
こういう小さな手当てをしていくと、
あなたの非同期コードは確実に「読めるコード」に育っていきます。
非同期処理を「動かせる」だけじゃなく、
「読めるように書ける」ようになったとき、
あなたはもう一段上のエンジニアになっています。

