期待されるエラーは値で返して網羅的に扱う
想定しているエラーを洗い出して型に列挙し、ワークフローの返り値を Result/Either にする。TypeScript なら neverthrow の ResultAsync を使う。
// 想定してるエラーを洗い出す
type GetTimelineError =
| { type: "USER_NOT_FOUND" }
| { type: "USER_SUSPENDED" }
| { type: "RATE_LIMITED"; retryAfter: number }
| { type: "INTERNAL_ERROR" };
// ワークフローの関数
type GetTimelineFunc = (userId: UserId, cursor?: string) => ResultAsync<Timeline, GetTimelineError>;
型シグネチャを先に決めておくと、エージェントの実装は以下のようになって型で意図を汲み取りやすくなる。
const getTimeline: GetTimelineFunc = (userId, cursor) => {
// 実装
};
// パターンマッチングで想定したエラーはすべてハンドリング(exhaustiveでないとエラーにする)
const handleGetTimeline = async (req: Request, res: Response) => {
const result = await getTimeline(req.userId, req.query.cursor);
result.match(
(timeline) => res.json(timeline),
(error) => {
switch (error.type) {
case "USER_NOT_FOUND":
return res.status(404).json({ error: "User not found" });
case "USER_SUSPENDED":
return res.status(403).json({ error: "Account suspended" });
case "RATE_LIMITED":
return res.status(429).json({ retryAfter: error.retryAfter });
case "INTERNAL_ERROR":
return res.status(500).json({ error: "Internal error" });
}
}
);
};
UI 側でも同じ形にする
アプリ側もエラーを sealed class で列挙しておく。
sealed class HomeError {
data object NetworkError : HomeError()
data object RateLimited : HomeError()
data object SessionExpired : HomeError()
data object Unknown : HomeError()
}
すると Compose 側はこう書ける。
@Composable
fun HomeScreen(viewModel: HomeViewModel) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
when (val state = uiState) {
is HomeUiState.Loading -> LoadingIndicator()
is HomeUiState.Success -> {
LazyColumn {
items(state.timeline) { item ->
when (item) {
is TimelineItem.Tweet -> TweetCard(item.tweet)
is TimelineItem.Retweet -> RetweetCard(item.retweetedBy, item.tweet)
is TimelineItem.Ad -> AdCard(item.ad)
}
}
}
}
is HomeUiState.Error -> {
// 設計時に想定されたエラーが漏れるとexhaustiveでなくなってコンパイルエラーが発生する
// Unknownなエラーが発生したときにどう対応すべきかを考えさせられる
when (state.error) {
is HomeError.NetworkError -> NetworkErrorMessage()
is HomeError.RateLimited -> RateLimitedMessage()
is HomeError.SessionExpired -> SessionExpiredMessage()
is HomeError.Unknown -> UnknownMessage()
}
}
}
}
どっちにしてもエラーハンドリングが漏れるなど設計意図からずれたら、コンパイルで気付かはる。
エラー以外の状態を型で潰す話は 想定外の状態は型で表現できなくする、なぜエージェントに対してこれをやるのかは 型はレビューで伝えていた知識を運ぶ に書いた。