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を実行するという意味。