本番と開発中で違うエラーメッセージを出す
(*説明が長いので、お急ぎの方は答え合わせをご覧ください...)
例えばデータベースのエラーを捕まえる try / catchがあったとしましょう。
(今回はReact Router v.7 RSC Framework mode, Vite, PostgreSQL, Drizzleを使って作っているアプリの例を紹介します)
export default async function deleteTennant(id: string): Promise<string | Error> {
// falsy string (null, undefined, empty) not allowed as id
if (!id) return new Error("Falsy tennant id");
try {
const deletedResult =await db.update(tennants)
.set({ deleted: true, dcid: DATACENTER_ID })
.where(eq(tennants.id, id))
.returning({deletedId: tennants.id});
return deletedResult[0].deletedId;
}
catch (e) {
return e;
}
この関数を使う側では、当然としてエラーが出たらそれを表示しようとするでしょう。
const deleteResult = await deleteTennant(id);
if (deleteResult instanceof Error)
return(<div>エラー:{deleteResult.message}</div>);
return(<div>レコードが削除されました</div>);
しかし、このエラーにはユーザーに不要で、攻撃者に見せたくない情報が入っています。where句をわざと(where(eq(USER.id,...)に変えてエラーを発生させたときのエラーメッセージは次のようなものでした。
DrizzleQueryError: Failed query: update "tennants" set "dcid" = $1, "last_chg" = $2, "deleted" = $3 where "users"."id" = $4 returning "id"
params: 999,2025-12-14T23:29:11.744Z,true,23b94b39-af61-423f-75f7-714bfd264429
at NodePgPreparedQuery.queryWithCache (file:///home/malaguenha-lnxon/....
{
query: 'update "tennants" set "dcid" = $1, "last_chg" = $2, "deleted" = $3 where "users"."id" = $4 returning "id"',
params: [
999,
'2025-12-14T23:29:11.744Z',
true,
'23b94b39-af61-423f-75f7-714bfd264429'
],
cause: error: missing FROM-clause entry for table "users"
at /home/ma....
貴重なtennantIdが...
こんな調子で、パスワードやセッションIDなども漏洩しそうです。
こんな場合、どうすればよいでしょう?
(すでにご存知かもしれませんが)生のエラーメッセージはユーザーにむやみに表示しないのが賢明です(それより、ユーザーにわかりやすい、「やさしくユーザーを導く」メッセージにします)。でも、デバッグのためにはメッセージ表示が必要なので、開発中は表示し、本番アプリでは表示しないようにします。
const errorMsg = (開発環境ですか?) ? e.message : "Internal server error - 削除できませんでした。他のユーザーがすでに削除した可能性があります。再発する場合やご不明の点についてはサポートページをご確認...";
こんなふうに、開発環境で実行しているのか、それも本番なのかによって、処理のフローを変えたい場合があり、エラーメッセージの抑制はその典型例です。
この(開発環境ですか?)スイッチの実体としては、例えば
サーバー側では(Node)
if (process.env.NODE_ENV === "development")...
クライアントコンポーネントなどでは(Viteのmetaを使って)
if (import.meta.env?.DEV === true)...
が使われています。
OpenAIさんやClaudeさんに聞いて調べたところ、このスイッチは以下のような場面で使われているようです。
- 「開発中」バナーを表示する
- 開発者向けのダッシュボードやツールパネルを表示する
- ログをどれだけ詳細にするか(how verbose should be)調整する
- 本番では使用しないモックや仮のサービスなどの切り替え*
- 開発の加速のためにアニメーションや当面不要な機能をOFFにする*
- 機能(フォールバック表示など)チェックのためにタイマーを入れて実行を遅らせる場合、このタイマーをON/OFFする*
*がついた用途では、該当する開発時専用コードを最終的なプロダクションビルドの前に完全に削除するのが望ましいでしょう。それでも一応、Viteは
if (import.meta.env.DEV) {
// この部分
}
meta.env.DEVで選別されたコードをデッドコードと判定し、ビルドから外し、その分プロダクションバンドルを縮小してくれます。
答え合わせ
今回の例で、deleteTennantを呼んだ後の処理は以下のように実装するのがよいでしょう(deleteTennantはloaderやactionから呼び出すものとします)。
const deleteResult = await deleteTennant(id);
if (deleteResult instanceof Error) {
// logは詳しく
console.error(deleteResult);
// RR7 RSCではloader/actionからreact elementを返せます
return (process.env.NODE_ENV === "development") ?
(<div>エラー:{deleteResult.message}</div>) :
(<div>削除できませんでした</div>);
}
}
return(<div>レコードが削除されました</div>);
参考文献
Vite公式ドキュメント、Env Variables and Modes - https://vite.dev/guide/env-and-mode
React Router 7公式ドキュメント、React Elements From Loaders/Actions - https://reactrouter.com/how-to/react-server-components#react-elements-from-loadersactions