LoginSignup
15
10

More than 5 years have passed since last update.

Promiseチェーンを途中でスキップしたい

Posted at

Expressアプリのコードを書いてるとき、Promise周りでうまくいかないことがあった。

例えば以下のようなコードがある。

app.get('/users/:id', (req, res) => {
  return User.findById(req.params.id)
    .then((user) => {
      if (!user) {
        return res.status(404).json({message: 'Not found'});
      }
      return user;
    })
    .then((user) => {
      return doSomething(user);
    })
    .then(() => {
      return res.status(200).json({message: 'Successfully processed'})
    })
    .catch((err) => {
      if (err.name === 'CastError') {
        return res.status(404).json({message: 'Not found'});
      }
      return res.status(500).json({message: 'Server error'});
    });
});

ユーザIDでユーザを検索し、ユーザが存在すればそのまま処理を継続、ユーザが存在しなければ 404 Not found を返すという処理。

Userモデルはmongooseを使っている想定で、mongooseはIDが存在しなければthenにnullを渡し、存在すればuserオブジェクトを渡す。
また、IDがMongoDBのObjectIDの形式になっていない場合はrejectedとなり、CastErrorを渡す。

ここで問題なのは、IDが存在しなかった場合、5行目のres.json()で処理を終了したいのだが、Promiseチェーンはここで終了せず次のthenに進んでしまうこと。

Promiseチェーンを途中で終了させるにはどうしたらよいのか?

throwする

Promiseチェーンはtry-catchのように動作するので、Promiseチェーンを終了させたい場所でthrowすればcatchに処理が移る。catchではエラーの種類によってレスポンスを変えるようにすれば良い。

app.get('/users/:id', (req, res) => {
  return User.findById(req.params.id)
    .then((user) => {
      if (!user) {
        throw new NotFound();
      }
      return user;
    })
    .then((user) => {
      return doSomething(user);
    })
    .then(() => {
      return res.status(200).json({message: 'Successfully processed'})
    })
    .catch((err) => {
      if (err instanceof NotFound) {
        return res.status(404).json(err);
      } else if (err.name === 'CastError') {
        return res.status(404).json({message: 'Not found'});
      }
      return res.status(500).json({message: 'Server error'});
    });
});

class NotFound extends Error {
  constructor(message) {
    super(message);
    this.description = message;
    this.name = 'NotFound';
    this.status = 404;
  }
}

ここではカスタムエラークラスを作っていて、instanceofでエラークラスを判定して処理をわけるという方法をとっている。

bluebirdを使う

Native PromiseにはないbluebirdのFiltered Catchを使うと、Javaなどの他のプログラミング言語で見るような書きかたができる。

...
const mongoose = require('mongoose');
const bluebird = require('bluebird');
mongoose.Promise = bluebird;  // Promiseにbluebirdを使う
...

app.get('/users/:id', (req, res) => {
  return User.findById(req.params.id)
    .then((user) => {
      if (!user) {
        throw new NotFound();
      }
      return user;
    })
    .then((user) => {
      return doSomething(user);
    })
    .then(() => {
      return res.status(200).json({message: 'Successfully processed'})
    })
    .catch(NotFound, (err) => {
      return res.status(404).json(err);
    })
    .catch({name: 'CastError'}, (err) => {
      return res.status(404).json({message: 'Not found'});
    })
    .catch((err) => {
      return res.status(500).json({message: 'Server error'});
    });
});

Filtered Catchはcatch(ErrorClass, handler)という書き方ができて、catchしたエラーがErrorClassのインスタンスだったら、handlerを実行する。そして、一つのcatchが実行されたら、その後のcatchは実行されない。

上のコードでいえば、NotFoundにマッチしたら、その後の2つのcatchは実行されない。

なお、catch({name: 'CastError'}, handler)は、catchに渡されたエラーオブジェクトerrのnameプロパティがCastErrorだったら、そのhandlerを実行するという意味。

参考

15
10
2

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
15
10