1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【JavaScript】async/awaitで返されるPromise

1
Posted at

はじめに

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()は特定の処理のエラーのみを扱いたい場合に使う
  • エラーを再スローすることで、呼び出し元でも処理できるようにする
1
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?