4月から大学院生になりました🤗単位の取得も必要でかつ研究もやらなくては行けないのですが、ブログは別腹ということで…😊今後は大学生たちのコード上の悩みもネタにさせていただこうと思います。

今回はJavaScriptの同期・非同期に関する話題です。自分も以前はC#を触っていたので、同期・非同期に関しては苦労していましたが、C#は同期的にも書けるという点ではそこまで問題ではなかったのですが、JavaScriptとなるとそうもいかずという感じですよね。
タイトルにリスキリングと入っていますが、正確さを求めるならリカレントなのかも🙄
- 1. 非同期処理
- 2. JavaScriptの非同期処理を理解する前の混乱
- コールバック関数の複雑さに頭を抱える日々
- 3. 救世主になるのか?Promiseの登場
- 4. そして、async/awaitとの運命の出会い
- 5. 実践あるのみ
- 6. つまずいたポイントと解決方法
- おわりに
1. 非同期処理
Pythonでコードを書いている私ですがジャンルによっては、JavaScriptに触れる機会が増えてきました。そこであまり会いたくなかった、非同期処理という曲者...。
研究室で見た「setTimeout()地獄」
研究室で学生たちがJavaScriptによる開発に取り組んでいる様子を見ていると、APIの処理結果が変数に反映されないという問題で苦労していました。
「データを取得してから次の処理をするために、setTimeout関数の中に入れて待ってるんだよね」と言っていましたが、ちょっと怪しい雰囲気が漂っていました。
「なぜデータが入っていないの?」という疑問
そんなときに、「これって非同期処理の問題じゃない?」と言ったのですが、、私もJavaScriptの非同期処理についてはほとんど理解してないので😭。やっぱり学び直し必要ですね。JavaScriptの非同期処理、特にasync/awaitについて学ぶことを決意しました。
2. JavaScriptの非同期処理を理解する前の混乱
同期と非同期の違いって何?
まずは「同期」と「非同期」の違い。Pythonなどでは基本的に処理が上から順次実行される「同期処理」が当たり前だったので、JavaScriptの世界観は最初かなり混乱します。
Pythonの世界でファイルを読み込むコード
with open('data.txt', 'r') as f: data = f.read() # ファイルを読み込み終わるまで次には進まない print(data) # データが確実に入っている
JavaScriptの世界で似た感じでファイルを読み込むコードを書いてみる(順番通りに上手くうごかない)
const fs = require('node:fs') fs.readFile('data.txt', 'utf8' ,(err, data) => { console.log(data); // データが読み込まれた後にここが実行されるはず }); console.log("ファイル読み込み中...?"); // 先にこっちが実行される!
「順番通りに実行されない」というのが混乱の元になります。
Pythonなどとの違いで戸惑ったこと
もう一つ戸惑ったのは、「コールバック関数」という概念が最初は全く理解できませんでした。
Pythonだと関数はこんな感じ
def get_data(url): response = requests.get(url) return response.json() data = get_data("https://api.example.com/data") # データを取得して返してくれる
JavaScriptだとこんな感じ(古い書き方?)かなと思います。
function getData(url, callback) {
fetch(url)
.then(response => response.json())
.then(data => callback(data));
}
getData("https://jsonplaceholder.typicode.com/todos/1", function(data) {
console.log(data); // ここでデータを受け取る
});
「なんで関数を引数として渡すの?」という素朴な疑問、「じゃあ返り値はどうなるの?」という混乱。これがJavaScriptの世界なのか…ということで、エディタをそっ閉じした自分がいました🥲
コールバック関数の複雑さに頭を抱える日々
そしてJavaScriptのコードを書こうとすると、いわゆる「コールバック地獄」に陥りました。処理Aが終わったら処理B、処理Bが終わったら処理C...という単純なことをやりたいだけなのに、コードのインデントは右へ...。
getData(function(data) { processData(data, function(processed) { saveData(processed, function(result) { displayData(result, function(display) { // もうインデントが嫌になる }); }); }); });
Pythonの順次実行に慣れていた私には、この入れ子構造は本当に読みづらく感じました。そう思いながらも、この時点では良い解決策が見つからずにいました。そしてまたしてもエディタをそっ閉じするのでした。
3. 救世主になるのか?Promiseの登場
「Promise(約束)」という概念
コールバック地獄からの開放の概念としてPromiseが登場しました。これで開放される🤩か?
Promiseの基本的な考え方は「将来的にデータを返します」という約束をするオブジェクト。例えばAPIにデータをリクエストする時、すぐには結果が返ってこないけど、「そのうち返すから、それまで待っといて!」という約束というような考え方です。
// Promiseの基本的な使い方 const myPromise = new Promise((resolve, reject) => { // 非同期処理(例:fetch(), fs.readFile(), setTimeout() など) // 以下ではこの処理の成功結果がdata、エラー結果がerrorとします。 if (/* 処理が成功した場合 */) { resolve(data); // 成功したらresolveを呼ぶ } else { reject(error); // 失敗したらrejectを呼ぶ } });
この考え方はなんとなく理解できましたが、実際にコードで使うとなるとまだ混乱していました🥲
thenとcatchの連鎖に挑戦
Promiseの良いところは、.then()メソッドで「約束が果たされた(処理が終わって値が返された)後」の処理を指定できること。これによってコールバック地獄からは少し抜け出せたか?🤔
先ほどのコールバック地獄のコードは以下のようになるでしょうか。インデントは深くはならないけども🤔
fetch("https://api.example.com/data") .then(response => response.json()) .then(data => { console.log(data); return processData(data); }) .then(processed => { console.log(processed); return saveData(processed); }) .then(result => { displayData(result); }) .catch(error => { console.error("エラー発生:", error); });
横に広がるのではなく、縦に連なるコードになったので、まだ、読みやすさは確実に向上しました。また、.catch()ですべてのエラーをまとめて処理できます。(これはこれでいいのだろうか?)
私としては、このレベルではまだまだエディタをそっ閉じしたいのです😭
まだ残る読みにくさの問題
Promiseを使うことで便利にはなったと思うのですが、これでもまだ私にとっては救世主にはなってくれないと感じます。複雑な処理になると、.then()の連鎖が長くなりすぎて、全体の流れを把握するのが難しくなるのです。また、条件分岐や繰り返し処理を組み込むと、さらに複雑になってしまいます。
fetchUser(userId) .then(user => { if (user.isAdmin) { return fetchAdminData(); } else { return fetchNormalData(); } }) .then(data => { // dataがadminDataなのかnormalDataなのか分かりにくい... return processData(data); }) // 以下続く...
「もっとシンプルに書けないのかな...」そんな思いを抱えたまま、JavaScriptとはおさらばしていました🙋。
4. そして、async/awaitとの運命の出会い
C#やPythonでもasync/awaitを書いていたので存在は知っていたのですが、JavaScriptは書きたくないなあと思っていましたが、 ようやく「これだ!」と感じました。
先ほどまでの見にくいコードがasync/awaitを使用すれば以下のように書き直せます。
async function fetchAndProcessData() { try { const response = await fetch("https://api.example.com/data"); const data = await response.json(); const processed = await processData(data); const result = await saveData(processed); return result; } catch (error) { console.error("エラー発生:", error); } }
「これ、普通のPythonみたいに読めるじゃん!」と思わず変な声が出ました。asyncとawaitというキーワードを使うことで、非同期処理なのにまるで同期処理のように書けるのです。これだよ欲しかったのはこれなんだよ😭。
同期処理のように書ける非同期コード
async/awaitの良さは、非同期でありながら「上から下へ」と順番に読めるコードが書けること。Promiseの.then()連鎖よりもさらに読みやすく、理解しやすいのです。
例えば、ユーザーデータを取得して処理する関数はこんな感じです。
async function handleUserData(userId) {
try {
// 各ステップが完了するのを待ってから次へ進む
const user = await fetchUser(userId);
let data;
if (user.isAdmin) {
data = await fetchAdminData();
} else {
data = await fetchNormalData();
}
const processed = await processData(data);
const result = await saveData(processed);
return result;
} catch (error) {
console.error("エラー発生:", error);
}
}
このコードは条件分岐があっても読みやすく、各ステップがどのような順序で実行されるかが分かりやすいです。
Pythonの非同期処理との類似点
このasync/await構文はPythonの非同期処理とかなり似ています。Pythonでも非同期処理を書く場合は、似たような記述になります。
async def handle_user_data(user_id): try: user = await fetch_user(user_id) if user.is_admin: data = await fetch_admin_data() else: data = await fetch_normal_data() processed = await process_data(data) result = await save_data(processed) return result except Exception as e: print(f"エラー発生: {e}")
「昔の話なんて知らなければもっとスッキリできたはずなのに!」と思いました。が、歴史的に仕方ない…。
5. 実践あるのみ
ここまできたら後はJavaScriptの非同期処理、特にasync/awaitについて実践あるのみです。
基本的な使い方
まず最初に、基本的な使い方から学びました。async/awaitを使うときのポイントは以下のとおり
‐ 非同期処理を行う関数はasyncキーワードを付ける ‐ Promiseの結果を待つ場合はawaitキーワードを付ける ‐ awaitはasync関数の中でしか使えない
簡単な例はこんな感じです。非同期処理を行う場合にはその処理を含む関数をasyncにして、Promiseを返す処理の前にawaitを付けるだけです。
// 基本的なasync関数 async function fetchData() { const response = await fetch('https://jsonplaceholder.typicode.com/todos/1'); const data = await response.json(); return data; } // async関数を呼び出す async function main() { const data = await fetchData(); console.log(data); } main();
エラーハンドリングの実装
次はエラーハンドリング。async/awaitを使う場合、通常のJavaScriptと同じようにtry/catchを使えます。
async function fetchWithErrorHandling() { try { const response = await fetch('https://jsonplaceholder.typicode.com/todos/1'); // HTTPエラーのチェック if (!response.ok) { throw new Error(`HTTPエラー: ${response.status}`); } const data = await response.json(); return data; } catch (error) { console.error('データ取得エラー:', error); // エラー時のフォールバック return { error: true, message: error.message }; } }
Promiseの.catch()と比べると、エラーが発生した場所が明確になり、デバッグもしやすくなります。また、エラー発生時の処理も書きやすいですね。
複数の非同期処理を扱う方法
非同期は並立に動作させることもできる利点がありますが、複数の非同期処理を効率的に扱う方法は2つのパターンがあります。それは「逐次実行」と「並行実行」になります。
逐次実行(処理を順番に行う)
async function sequential() { console.time('sequential'); const data1 = await fetchData('/api/data1'); const data2 = await fetchData('/api/data2'); const data3 = await fetchData('/api/data3'); console.timeEnd('sequential'); return [data1, data2, data3]; }
並行実行(処理を同時に開始する)
Promise.all()を使用することで複数の非同期処理を同時に実行開始できます。
ちなみにPromise.all()は以下の機能を持っています。
‐ Promise.all()は引数のすべてのPromiseが成功した場合のみ成功し、1つでも失敗するとすぐに失敗する(all-or-nothing) ‐ 結果は配列として返され、元のPromise配列と同じ順序で結果が格納される ‐ 1つでも失敗すると、最初に失敗したPromiseのエラーが返される
async function parallel() { console.time('parallel'); // すべてのPromiseを同時に実行開始 const results = await Promise.all([ fetchData('/api/data1'), fetchData('/api/data2'), fetchData('/api/data3') ]); console.timeEnd('parallel'); return results; }
上記の2つの関数を実行して時間を計測すると、並行実行の方が速いことが分かりました。ただし、処理の順序に依存関係がある場合(例:前の処理の結果を次の処理で使う)は逐次実行にする必要があります。
6. つまずいたポイントと解決方法
実際にコードを書いてみると、いくつかつまずくポイントがありました。ここでは私が経験した主な問題と解決方法を共有します。
よくあるミスと対処法
1. awaitを忘れる
最もよく起こるミスの一つが、awaitキーワードを忘れることです🥲
async function buggyFunction() { // ❌ awaitを忘れている const response = fetch('https://api.example.com/data'); console.log(response); // Promiseオブジェクトが出力される // ✅ 正しい使い方 const correctResponse = await fetch('https://api.example.com/data'); console.log(correctResponse); // レスポンスオブジェクトが出力される }
awaitを忘れると、Promise自体が返されるため、期待した値ではなくなります。「なんでデータが入ってないの?」という問題の多くはこれが原因です。
2. async関数の外でawaitを使う
awaitは必ずasync関数の中でしか使えません。 この問題はよく遭遇しますし、変更が面倒臭いかもしれません。
// ❌ トップレベルでawaitは使えない const data = await fetchData(); // SyntaxError // ✅ async関数の中で使う async function init() { const data = await fetchData(); // ... } init(); // ✅ または即時実行関数(定義してすぐ呼ぶ使い捨ての関数)を使用する (async () => { const data = await fetchData(); // ... })();
3. エラーハンドリングを忘れる
エラーハンドリングを忘れると、ユーザーに適切な処理が行えない可能性があります。
// ❌ 間違い:エラーハンドリングがない async function noErrorHandling() { const data = await fetchData(); // エラーが起きると関数全体が失敗 return processData(data); } // ✅ 正しい使い方:try/catchでエラーをハンドリング async function withErrorHandling() { try { const data = await fetchData(); return processData(data); } catch (error) { console.error('エラーが発生:', error); return defaultData; // エラー時の対応 } }
「あ、awaitを忘れてる!」という気づき
一番よく遭遇するのは「あ、awaitを忘れてる!」という瞬間です。コードを書いていて、なぜかPromiseオブジェクトが出力されている...そんな時はほぼ間違いなくawaitキーワードを忘れています。
これに気づけるようになったのは、「async関数は常にPromiseを返す」「awaitはPromiseの結果を取り出す」という概念を理解できたからです。非同期処理の基本概念を理解すると、バグの原因も特定しやすくなります。
おわりに
JavaScriptの非同期処理、特にasync/awaitの復習を終えて、最初は難しく感じた概念も今ではかなり理解できるようになりました。Pythonとの類似点を見つけられたことで、学習のハードルも下がったように感じます。
特に印象的だったのは、「非同期処理」という概念自体は難しくないということ。要するに「時間のかかる処理を待っている間に他のことをやる」という考え方です。これは日常生活でもよくあること(例:洗濯機を回している間に掃除をするなど)なので、イメージしやすくなると思います。
研究室の人には以下のように助言したいと思います。
‐ データが取得できるまで待つなら、Promiseやasync/awaitを使おう ‐ コールバック関数だけに頼らない: 新しいコードを書くなら、積極的にasync/awaitを活用しよう ‐ 「awaitを忘れていないか」をチェックすることで、多くの「データが入ってない問題」が解決できる
非同期処理の世界は最初は難しく感じますが、コツを掴めば便利で強力なツールなりそうです。 自分もようやくエディタのそっ閉じはなくなりそうです😊