JavaScript | 非同期処理:設計・理解の深化 - 非同期コードの可読性

JavaScript JavaScript
スポンサーリンク

なぜ「非同期コードの可読性」がそんなに大事なのか

非同期処理って、動かすだけなら意外とすぐ書けます。
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();
}
JavaScript
async 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;
}
JavaScript

getUser は「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 の位置を整理する
・コメントで意図を書く

こういう小さな手当てをしていくと、
あなたの非同期コードは確実に「読めるコード」に育っていきます。

非同期処理を「動かせる」だけじゃなく、
「読めるように書ける」ようになったとき、
あなたはもう一段上のエンジニアになっています。

タイトルとURLをコピーしました