CRMで20件の連絡先をアーカイブするはずが、UIには緑色の成功バッジが表示されたものの、実際には何も行われずに処理が終了してしまった。Supabase-JSのミューテーションにおいてerrorフィールドを必須にするTypeScriptヘルパーを使えば、開発者はその失敗を「なかったこと」にするのではなく、正面から向き合うことができるようになる。
なぜSupabase-JSの戻り値の形式は「罠」なのか
SupabaseのJavaScriptクライアント(@supabase/supabase-js)は、データベースへの書き込みが拒否されても例外(exception)をスローしません。その代わりに、{ data, error } というオブジェクトでプロミスを解決(resolve)します。行レベルセキュリティ(RLS)ポリシー、一意制約(unique-key)違反、あるいはその他の制約によってクエリがブロックされた場合、data は null となり、error にデータベースのメッセージが含まれます。クライアントは、呼び出し側が error を検査することを前提としており、呼び出し自体を中断することはありません。
実際、多くのコードベースでは、この呼び出しを「投げっぱなし(fire-and-forget)」の操作として扱っています。
await supabase
.from('contacts')
.update({ statut: 'ancien', archived_at: now })
.eq('id', id)
return { ok: true }
update がRLSルールによってブロックされても、プロミスは解決されます。開発者が { error } を分割代入(destructure)していなければ、失敗は目に見えません。関数は { ok: true } を返し、UIは成功を表示し、データは変更されないまま残ります。ログも出力されず、Sentryのアラートも飛ばず、バグは数日間放置される可能性があります。
リンターだけでは不十分
静的解析ツールは error プロパティが無視されている場合に警告を出すことはできますが、実行時の契約(runtime contract)を強制することはできません。開発者は依然として const _ = await … と書いて警告を黙らせたり、コメントを追加してルールを抑制したりできてしまいます。根本的な問題は、型システムが error に一切触れずに呼び出しを成功させてしまうことを許容している点にあります。
mutate() ヘルパー:エラーハンドリングを強制する
著者は、戻り値の型を変更する mutate() という小さなラッパーを作成しました。{ data, error } の代わりに、このヘルパーは error を必須フィールドとするタプル [data, error] を返します。これにより、TypeScriptは2番目の要素を破棄するような呼び出しをコンパイル時に拒否します。
async function mutate<T>(promise: Promise<{ data: T | null; error: any }>) {
const { data, error } = await promise
// Send error to Sentry immediately
if (error) Sentry.captureException(error)
return [data, error] as const
}
使い方は明示的になります:
const [result, err] = await mutate(
supabase
.from('contacts')
.update({ statut: 'ancien', archived_at: now })
.eq('id', id)
)
if (err) {
// Handle or rethrow
return { ok: false, message: err.message }
}
return { ok: true, data: result }
もし開発者が err のキャプチャを忘れると、TypeScriptは「Tuple type [T, any] of length 2 has no element at index 1」というエラーを出力します。エラーに対処するまで、コードはコンパイルできません。また、このヘルパーはSentryのインストルメンテーション(計測機能)を注入するため、呼び出し側が後でエラーを飲み込んだとしても、すべてのデータベース拒否が確実にログに記録されます。
何が懸かっているのか
- データの整合性 – サイレントな失敗は、不正な状態を本番環境に紛れ込ませます。アーカイブしたはずの連絡先がアクティブリストに残っていると、後続のレポートに誤りが生じる可能性があります。
- ユーザーの信頼 – 何も変わっていないのに成功を主張するUIは、信頼を損ないます。顧客は「アーカイブ済み」と表示されているのに、検索結果にその連絡先が出てきてしまうといった事態を招きます。
- 開発者の時間 – 実体のないバグ(phantom bugs)の追跡には膨大な時間が費やされます。明示的なエラーハンドリングは、失敗した時点で問題を表面化させ、デバッグのループを短縮します。
- 運用コスト – 数行のコードと小さなラッパーを追加するコストは、サイレントなデータ損失が発生した際のコストに比べれば微々たるものです。
トレードオフ
このヘルパーは冗長性を増大させます。すべてのミューテーション呼び出しがタプルを返すようになり、呼び出し側は if (err) ブロックを書かなければなりません。特に、成功を前提としている単純なCRUD操作において、一部のチームはこれを「ボイラープレート的なノイズ」と見なすかもしれません。しかし、これに対する反論は、この追加コードは「オプションの機能」ではなく「セーフガード(安全装置)」であるという点です。CRMシステム、金融、ヘルスケアなど、データの正確性が極めて重要な環境では、このチェックを強制することの価値は十分にあります。
次回予告
著者は、説明なしにゼロ行を返すRLSポリシーを検証する第3回を予定しています。そのパターンも、サイレントエラーのケースと同様に、一見成功しているクエリの背後に失敗を隠してしまいます。このシリーズは、Supabase APIの「噂(rumor)」の部分を暴き、開発者が事実を突き止めるための具体的なツールを提供することを目指しています。
まとめ: Supabase-JSの設計では、error フィールドを読み取ることを忘れない限り、書き込みの失敗が成功のように見えてしまいます。error を必須にし、Sentryにログを記録するTypeScript強制型のヘルパーで呼び出しをラップすることで、サイレントな失敗を「目に見える、対処可能なイベント」へと変えることができます。コード量のわずかな増加と引き換えに、サイレントな成功では決して得られない「データの信頼性」と「ユーザーの信頼」を手に入れることができるのです。
