【JavaScript】初心者向けにPromiseの使い方を解説する( then, catch, all, race, finaly)

Promise-Basic

こんにちは、フロントエンドエンジニアのまさにょんです。

今回は、JavaScriptでPromiseについて解説します。

非同期処理とは?

非同期処理とは、1つの処理タスクの実行が完了しなくても次の処理に移行する(バックグラウンド処理する)処理フローのことです。

これは、同期処理とは正反対の意味で、まずは同期処理を理解すると意味がわかりやすいので、同期処理について説明します。

同期処理は、1つの処理タスクの実行が完了してから次の処理に移行する処理フローのことです。

処理の順番が明確に決まっており、1つの処理が完了したら、次に進む順序の決まった処理です。

つまり、複数の処理を実行する際でも、1つずつ順番に処理が実行されます。

これに対して、非同期処理では、1つの処理タスクの実行完了を待たずに次の処理に移行する処理フローのことです。

非同期処理では、処理の順番は決まっておらず、バックグラウンドで処理が進み、終わった順にその処理結果が反映されていきます。

つまり、複数の処理を実行する際に、処理の完了を待たずにバックグラウンド処理になり、他の処理が実行される処理フローです。

JavaScriptにおける非同期処理

JavaScriptは非同期言語であるため、1つ前の実行に時間がかかった場合、実行完了をまたずに次の処理が行われてしまいます。

1番わかりやすいのが、setTimeout() を使用した時間のかかる処理です。

console.log("1番目");

// 1秒後に実行する処理
setTimeout(() => {
  console.log("2番目(1秒後に実行される非同期処理)");
}, 1000);

console.log("3番目");

// [ 実行結果 ]
// 1番目
// 3番目
// 2番目(1秒後に実行される非同期処理)

Promiseは、処理の順序の「秩序」(約束・ルール)を作る

Promise を日本語に翻訳すると「約束」です。

処理の順序に「お約束」(ルール・秩序)を取り付けることができるものが Promise だと考えるとわかりやすいです。

Promiseを使用することで、resolve関数 または、reject関数 のどちらかが実行されるまで処理が待機されます。

また、resolve関数reject関数 かのどちらが使用されたかによって、その後に続く処理を分岐することができます。

resolve() で処理が終了(成功)したことを伝えると then() の処理に続きます。

reject() で処理が処理が終了(失敗)したことを伝えると catch() の処理に続きます。

上記のような処理の順序に「お約束」(ルール・秩序)を作ることができるのが、Promiseの特徴です。


console.log("1番目");

let successFlag = true;

// お約束を取り付けたい処理にPromise
new Promise((resolve, reject) => {

  //1秒後に実行する処理
  setTimeout(() => {
    console.log("2番目(1秒後に実行される非同期処理)");

    if (successFlag) {
        // resolve() で処理が終了(成功)したことを伝える => then() の処理に続く・・・
        resolve();
    } else {
        // reject() で処理が終了(失敗)したことを伝える => catch() の処理に続く・・・
        reject();
    }
    
  }, 1000);
})
.then(() => {

  // 処理が無事終わったことを受けとって実行される処理
  console.log("resolve時(成功時)に実行される処理: 3番目");
})
.catch(()=> {

  console.log("reject時(失敗時)に実行される処理: 3番目");
});

上記のSampleCodeをDevToolsのコンソールで実行すると次のような結果が出力されます。

Promiseによって、2番目の処理が完了してから、3番目の処理に移行しているのがわかります。

Promiseの仕組みを理解する

ここからは、Promiseの仕組みなどを細かく見ていきます。

Promiseには3つの状態がある

まず理解しておくべきPromiseの特徴として、3つの状態 (status)があります。

Promise には、PromiseStatus というstatusがあり、

Promise の状態は、常に3つのstatus のいずれかになります。

  1. 待機 (pending): 初期状態。成功も失敗もしていません。
  2. 履行 (fulfilled): 処理が完了したことを意味します。
  3. 拒否 (rejected): 処理が失敗したことを意味します。

new Promise()で作られたPromiseオブジェクトは、pendeingというPromiseStatusで作られます。

処理が成功 (resolve関数の実行)した時に、PromiseStatusfulfilledに変わり,thenに書かれた処理が実行されます。

処理が失敗 (reject関数の実行)した時は、PromiseStatusrejectedに変わり、catchがあれば catchの処理が実行されて、PromiseStatusfulfilled に変わります。

上記の特徴を理解した上で、Promiseの書き方などを見ていきましょう。

Promiseの書き方

Promiseの基本構文は、次のとおりです。

// [ Promiseの書き方 ]
// Promiseインスタンスの作成・構文

const promise = new Promise((resolve, reject) => {
    console.log('何かの処理');
}).then(()=> {
    console.log('成功時の処理 (resolve関数呼び出し後に実行される処理)');
}).catch(()=> {
    console.log('失敗時の処理 (reject関数呼び出し後に実行される処理)');
});

まずはnew Promise() で Promiseのインスタンスを生成して、引数にはコールバック関数を設定します。

また、コールバック関数の第1引数は resolve関数、第2引数は reject関数を設定します。

この resolve関数reject関数の名前は自由に設定できますが、resolvereject にするのが一般的です。

第1引数の resolve関数は、成功時の then に処理を引き継ぎます。

つまり、処理が終わり、無事成功した状態に Promise の PromiseStatus を更新して、then に処理が移行するわけです。

第2引数の reject関数は、失敗時の catch に処理を引き継ぎます。

つまり、処理が失敗に終わってしまった状態に Promiseの PromiseStatus を更新して、catch に処理が移行するわけです。

resolve関数を実行する

まずは、resolve関数の実行結果を見てみましょう。

const promise = new Promise((resolve) => {
    resolve();
}).then(() => {
    console.log("処理成功: resolve");
});

console.log(promise);

resolve関数の引数に値を渡すと、それは then のコールバック関数で引数として受け取れます。

また、最終的に return をすることで、return による実行結果(返り値)が PromiseResult となります。

const promise = new Promise((resolve) => {
    resolve("処理成功: resolve");
}).then((val) => {
    return val;
});

console.log(promise);

reject関数を実行する

次に、reject関数の実行結果を見てみましょう。

reject関数を使用すると、PromiseStaterejected に変わります。

そして、catch節がないとエラーを発生します。

const promise = new Promise((resolve, reject) => {
    reject('処理失敗: reject');
});

console.log(promise);

reject関数を使用すると、PromiseStaterejected に変わりますが、

その後tのcatch節で処理が実行されると PromiseStatefulfilled に変わります。

const promise = new Promise((resolve, reject) => {
    reject('処理失敗: reject');
})
.catch((error) => {
    console.log(error);
});

console.log(promise);

Promiseのメソッドチェーン: then()やcatch()の後に、thenをさらに続ける

resolve (処理成功)した時に then に書かれた処理が実行され、

reject (処理失敗)した時は catch に書かれた処理が実行されますが、

それらの処理の後にまた別の then を実行することができます。

このような Promisethen() catch() の後に、then() をさらに続けて処理をすることを Promiseのメソッドチェーンと言います。

もちろん、通常どおり、return で返した値を第一引数として次の then に渡すことが可能です。

次のSampleは、resolve()then()した後に、 さらにthen() でメソッドチェーンをしています。

// Promiseのメソッドチェーン: then()やcatch()の後に、thenをさらに続けることができる!

// 1. resolveした場合
new Promise((resolve, reject) => {
    resolve("resolve: 処理成功");
})
.then((val) => {
    console.log(`then1: ${val}`);
    return val;
})
.catch((val) => {
    console.log(`catch: ${val}`);
    return val;
})
.then((val) => {
    console.log(`then2: ${val}`);
});

// [ 実行結果 ]
// then1: resolve: 処理成功
// then2: resolve: 処理成功

こちらは、reject()catch()した後に then()でメソッドチェーンしています。


// 2. rejectした場合
new Promise((resolve, reject) => {
    reject("reject: 処理失敗");
})
.then((val) => {
    console.log(`then1: ${val}`);
    return val;
})
.catch((val) => {
    console.log(`catch: ${val}`);
    return val;
})
.then((val) => {
    console.log(`then2: ${val}`);
});

// catch: reject: 処理失敗
// then2: reject: 処理失敗

Promise.all

Promise.all() は配列でPromiseオブジェクトを渡し、すべてのPromiseオブジェクトが resolved になったら次の処理に進みます。

すべての処理が適切に終了したことを確認したうえで、新しい処理を行いたい場合に Promise.all() は使えます。

Promise.all() の引数では、配列の形でPromiseたちの実行結果(返り値)を受け取れます。

// < Promise.all() >

const promise1 = new Promise((resolve) => {
  setTimeout(() => {
    resolve();
  }, 1000);
}).then(() => {
  console.log("promise1おわったよ!");
  return "promise1";
});

const promise2 = new Promise((resolve) => {
  setTimeout(() => {
    resolve();
  }, 3000);
}).then(() => {
  console.log("promise2おわったよ!");
  return "promise2";
});

// 配列でPromise-Objectを渡す! => すべてがresolveになったら、then の処理に移行する!
Promise.all([promise1, promise2])
  .then(([val1, val2]) => {
    console.log(`${val1}&${val2}完了、Promise全部おわったよ!`);
  });

// [ 実行結果 ]
// promise1おわったよ!
// promise2おわったよ!
// promise1&promise2完了、Promise全部おわったよ!

const promiseA = new Promise((resolve, reject) => {
    resolve(123)
  });
  
  const promiseB = new Promise((resolve, reject) => {
    resolve('string')
  });
  
  const promiseC = new Promise((resolve, reject) => {
    resolve(true)
  });
  
  Promise.all([promiseA, promiseB, promiseC]).then((results) => {
    console.log(results);
  });

// < 実行結果 >
// [123, 'string', true]

Promise.all() は、1つでも処理が失敗すると実行されません。

const promiseA = new Promise((resolve, reject) => {
  resolve(123)
});

const promiseB = new Promise((resolve, reject) => {
  resolve('string')
});

const promiseC = new Promise((resolve, reject) => {
  reject(false)
});

// promiseCがrejectされたため、実行されない
Promise.all([promiseA, promiseB, promiseC]).then((results) => {
  console.log(results);
});

// < 実行結果 >
//   [[Prototype]]: Promise
//   [[PromiseState]]: "rejected"
//   [[PromiseResult]]: false
//   Error: Uncaught (in promise) false

Promise.race

Promise.race() は、Promise.all()と同じく配列でPromiseオブジェクトを渡し、どれか1つのPromiseオブジェクトがresolvedになったら次に進みます。

ちなみに、then() の引数には、先に処理が完了したどちらかの実行結果が渡されます。

raceは「競争・追いかけっこ」という意味です。

// < Promise.race >

const promise1 = new Promise((resolve) => {
    setTimeout(() => {
      resolve();
    }, 1000);
  }).then(() => {
    console.log("promise1おわったよ!");
    return "promise1";
  });
  
  const promise2 = new Promise((resolve) => {
    setTimeout(() => {
      resolve();
    }, 3000);
  }).then(() => {
    console.log("promise2おわったよ!");
    return "promise2";
  });
  
  // 配列でPromise-Objectを渡す! => どれか1つのPromiseオブジェクトがresolvedになったら次の処理に移行する!
  Promise.race([promise1, promise2])
  .then((val) => {
    // thenの引数には、先に処理が完了したどちらかの実行結果が、渡される。
    console.log(`${val}が先に終わったよ!`);
  });

// [ 実行結果 ]
//   promise1おわったよ!
//   promise1が先に終わったよ!
//   promise2おわったよ!

Promise.finally

finallyメソッドとは、処理の成功・失敗に関わらず、その先の処理を継続して行うメソッドです。

Promiseチェーンのさいごに必ず呼び出したい処理などを定義することができます。

thenメソッドとの違いは、Promiseの処理結果を判断する必要がない点です。

成功したか失敗したかは関係なく、Promiseが確定された段階で処理を行うということになります。

thenメソッドでは、処理が成功した場合と失敗した場合の処理を記述するためには、引数を2つ用意する必要があります。

// [ thenの場合 ]

const promise = new Promise((resolve, reject) => {
    const successFlag = false;
    if (successFlag) {
      resolve('成功');
    } else {
      reject('失敗')
    }
})
.then(
      result => console.log(result),
      error => console.log(error)
);

// [ 実行結果 ]
// 失敗

しかし、finallyメソッドを使えば、結果に関係なく実行したい処理を実行することができます。

// [ finallyの場合 ]

const promise = new Promise((resolve, reject) => {
  const successFlag = false;
  if (successFlag) {
    resolve('成功');
  } else {
    reject('失敗');
  }
})
.then(
  result => console.log(result),
  error => console.log(error)
)
.finally(() => console.log('結果に関係なく処理'));

// [ 実行結果 ]
// 失敗
// 結果に関係なく処理

thenメソッドのエラーハンドリングと catchメソッドは、どちらが優先されるのか?

thenメソッドのエラーハンドリングと catchメソッドでは、thenメソッドのエラーハンドリングの方が優先されます。


const promise = new Promise((resolve, reject) => {
    const successFlag = false;
    if (successFlag) {
        resolve('成功');
    } else {
      reject('失敗');
    }
  })
  .then(
    (result)=>{
        console.log('then', result);
    },
    (error) => {
        console.log('then', error); // こちらが優先される!
    }
  )
  .catch((error)=>{ 
    // catchメソッドは、rejectedステータスのPromiseオブジェクトを受け取ります。
    // 実質的には、thenメソッドでrejectedステータスを扱う場合と同じですが、コードが簡潔化されます。
    console.log('catch', error);
  })
  .finally(() => console.log('結果に関係なく処理'));

  // [ 実行結果 ]
  // then 失敗
  // 結果に関係なく処理

JavaScript書籍 Ver. 中級-上級者向け

JavaScript書籍 Ver. 初級者向け

Twitterやってます!Follow Me!

神聖グンマー帝国の逆襲🔥

神聖グンマー帝国の科学は、世界一ぃぃぃぃぃぃ!!!!!

参考・引用

  1. 【ES6】 JavaScript初心者でもわかるPromise講座
  2. 【JavaScriptの応用】Promise -finally・Promise.all
  3. Promise.prototype.finally()

最近の投稿