はじめに
async/awaitとPromiseの関係
async/awaitは、Promiseをより直感的に扱うための構文です。しかし、async/awaitを使う際に見落とされがちなのが、「async関数は必ずPromiseを返す」という重要な特性です。
この記事では、async関数がどのようにPromiseを返すのか、そしてその返り値をどのように扱うべきかを詳しく解説します。
この記事で学べること
- async関数が返す値の正体
- awaitキーワードがどのように動作するのか
- async関数の返り値を正しく受け取る方法
- エラー時の挙動とPromiseの関係
async/awaitの基本的な使い方は知っているけれど、内部の仕組みまでは理解していないという方に最適な内容です。
async関数の基本的な仕組み
async関数は必ずPromiseを返す
async関数は、どんな値を返しても必ずPromiseでラップされます。これはasync関数の最も重要な特性です。
async function getValue() {
return 'Hello';
}
console.log(getValue()); // Promise { 'Hello' }
上記のコードでは、文字列を返しているように見えますが、実際にはPromiseオブジェクトが返されます。
明示的な返り値とPromiseの関係
async関数内でreturnした値は、自動的にPromise.resolve()でラップされます。
// この2つは同じ動作
async function getNumber() {
return 42;
}
function getNumber() {
return Promise.resolve(42);
}
つまり、async関数は通常の関数のように値を返しているように見えて、実際にはPromiseを返しているのです。
asyncを付けるとどう変わるのか
関数にasyncキーワードを付けると、以下の変化が起こります。
// 通常の関数
function normalFunction() {
return 'value';
}
console.log(normalFunction()); // 'value'
// async関数
async function asyncFunction() {
return 'value';
}
console.log(asyncFunction()); // Promise { 'value' }
async関数が返すPromiseの種類
async関数は、返す内容によって異なる種類のPromiseを生成します。それぞれのパターンを見ていきましょう。
値を返す場合
一般的な値を返す場合、Promise.resolveでラップされます。
async function getString() {
return 'テキスト';
}
async function getObject() {
return { name: 'Taro', age: 25 };
}
async function getArray() {
return [1, 2, 3];
}
// すべてPromiseを返す
getString().then(value => console.log(value)); // 'テキスト'
getObject().then(value => console.log(value)); // { name: 'Taro', age: 25 }
getArray().then(value => console.log(value)); // [1, 2, 3]
何も返さない場合
明示的にreturnしない、またはreturn文がない場合、Promise.resolve(undefined)が返されます。
async function noReturn() {
console.log('処理を実行');
// return文なし
}
async function emptyReturn() {
console.log('処理を実行');
return;
}
noReturn().then(value => console.log(value)); // undefined
emptyReturn().then(value => console.log(value)); // undefined
Promiseを返す場合
async関数内でPromiseを返すと、そのPromise自体が返されます。二重にラップされることはありません。
async function getPromise() {
return Promise.resolve('データ');
}
// Promise.resolve(Promise.resolve('データ'))ではなく
// Promise.resolve('データ')が返される
getPromise().then(value => console.log(value)); // 'データ'
これは重要な仕様で、以下のように動作します。
async function fetchData() {
return fetch('https://api.example.com/data');
}
// fetchが返すPromiseがそのまま返される
fetchData()
.then(response => response.json())
.then(data => console.log(data));
エラーをthrowする場合
async関数内でエラーをthrowすると、Promise.rejectが返されます。
async function throwError() {
throw new Error('エラーが発生しました');
}
// Promise.reject(Error)が返される
throwError().catch(error => {
console.error(error.message); // 'エラーが発生しました'
});
awaitの動作とPromise
awaitはPromiseの解決を待つ
awaitキーワードは、Promiseが解決される(FulfilledまたはRejectedになる)まで処理を一時停止します。
async function example() {
console.log('開始');
const result = await Promise.resolve('完了');
console.log(result);
console.log('終了');
}
example();
// 出力:
// 開始
// 完了
// 終了
awaitは、Promiseの解決を待ってから次の行に進むため、同期処理のように見えるコードが書けます。
awaitが返す値の正体
awaitは、Promiseが解決された際の値を返します。つまり、Promiseのラッパーを外して中身の値を取り出す役割を持ちます。
async function getData() {
// Promiseを返す関数
const promise = Promise.resolve('データ');
// awaitでPromiseを解決し、中の値を取得
const data = await promise;
console.log(data); // 'データ' (Promiseではない)
return data;
}
以下の図で、awaitの動作を理解しましょう。
await無しで呼び出した場合の挙動
async関数をawait無しで呼び出すと、Promiseがそのまま返されます。
async function getValue() {
return 100;
}
// await無し
const result1 = getValue();
console.log(result1); // Promise { 100 }
// awaitあり
async function callGetValue() {
const result2 = await getValue();
console.log(result2); // 100
}
この違いを理解することは、async/awaitを正しく使う上で重要です。
async function processData() {
return 'データ';
}
// 間違った使い方
function wrongUsage() {
const data = processData();
console.log(data.length); // エラー: Promiseにlengthプロパティはない
}
// 正しい使い方
async function correctUsage() {
const data = await processData();
console.log(data.length); // 3
}
async関数の返り値を受け取る方法
async関数の返り値を受け取るには、複数の方法があります。状況に応じて適切な方法を選びましょう。
.then()で受け取る
async関数はPromiseを返すため、.then()メソッドで結果を受け取れます。
async function fetchUser() {
return { id: 1, name: 'Taro' };
}
// .then()で受け取る
fetchUser().then(user => {
console.log(user.name); // 'Taro'
});
複数のasync関数を連続して呼び出す場合も、.then()チェーンが使えます。
async function getUser(id) {
return { id: id, name: 'Taro' };
}
async function getUserPosts(userId) {
return [{ id: 1, title: '記事1' }];
}
getUser(1)
.then(user => {
console.log('ユーザー:', user.name);
return getUserPosts(user.id);
})
.then(posts => {
console.log('投稿数:', posts.length);
});
awaitで受け取る
async関数内では、awaitを使って返り値を受け取るのが一般的です。
async function fetchUser() {
return { id: 1, name: 'Taro' };
}
async function displayUser() {
const user = await fetchUser();
console.log(user.name); // 'Taro'
}
awaitを使うことで、同期処理のように読みやすいコードが書けます。
async function processUserData() {
// 順番に処理を実行
const user = await getUser(1);
const posts = await getUserPosts(user.id);
const comments = await getComments(posts[0].id);
return {
user,
posts,
comments
};
}
返り値を変数に代入する際の注意点
async関数の返り値を変数に代入する際は、awaitの有無によって結果が異なります。
async function getData() {
return 'データ';
}
// パターン1: awaitなしで代入
async function example1() {
const result = getData();
console.log(result); // Promise { 'データ' }
}
// パターン2: awaitありで代入
async function example2() {
const result = await getData();
console.log(result); // 'データ'
}
// パターン3: .then()で代入
function example3() {
getData().then(result => {
console.log(result); // 'データ'
});
}
トップレベル(async関数の外)では、awaitは使えないため、.then()を使うか、即時実行async関数で囲む必要があります。
async function getData() {
return 'データ';
}
// トップレベルでawaitは使えない(エラー)
// const data = await getData(); // SyntaxError
// 方法1: .then()を使う
getData().then(data => {
console.log(data);
});
// 方法2: 即時実行async関数
(async () => {
const data = await getData();
console.log(data);
})();
エラーハンドリングとPromise
async関数内でのエラーハンドリングは、Promiseの仕組みと密接に関係しています。
try-catchとPromise.reject
async関数内でエラーをthrowすると、そのエラーはPromise.rejectとして返されます。
async function riskyOperation() {
throw new Error('処理に失敗しました');
}
// Promise.rejectが返される
riskyOperation().catch(error => {
console.error(error.message); // '処理に失敗しました'
});
async関数内では、try-catchを使ってエラーをキャッチできます。
async function safeOperation() {
try {
const result = await riskyOperation();
return result;
} catch (error) {
console.error('エラーをキャッチ:', error.message);
return null; // デフォルト値を返す
}
}
エラーをthrowした場合の返り値
エラーをthrowすると、その時点でRejectedなPromiseが返されます。
async function validateData(data) {
if (!data) {
throw new Error('データが存在しません');
}
if (data.length === 0) {
throw new Error('データが空です');
}
return data;
}
// 使用例
validateData(null)
.then(result => {
console.log('成功:', result);
})
.catch(error => {
console.error('失敗:', error.message); // 'データが存在しません'
});
.catch()とtry-catchの使い分け
エラーハンドリングには、.catch()とtry-catchの2つの方法があります。それぞれ適した場面があります。
try-catchが適している場合
async function processMultipleSteps() {
try {
const step1 = await fetchData();
const step2 = await processData(step1);
const step3 = await saveData(step2);
return step3;
} catch (error) {
// すべてのステップのエラーを一箇所で処理
console.error('処理中にエラー:', error);
return null;
}
}
catchが適している場合
async function fetchUser(id) {
return fetch(`/api/users/${id}`)
.then(response => response.json())
.catch(error => {
// 特定の処理のエラーのみハンドリング
console.error('ユーザー取得エラー:', error);
return { id: id, name: 'Unknown' }; // デフォルト値
});
}
両方を組み合わせることもできます。
async function robustFetch() {
try {
const response = await fetch('/api/data')
.catch(networkError => {
console.error('ネットワークエラー:', networkError);
throw new Error('接続に失敗しました');
});
if (!response.ok) {
throw new Error(`HTTPエラー: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('データ取得エラー:', error);
throw error; // 呼び出し元でも処理できるよう再スロー
}
}
エラーハンドリングの基本原則として、以下を覚えておきましょう。
- try-catchは複数の処理をまとめてエラーハンドリングしたい場合に使う
- .catch()は特定の処理のエラーのみを扱いたい場合に使う
- エラーを再スローすることで、呼び出し元でも処理できるようにする