値がある保証だと思って付けていた「!」は、確認しないと決めた印でした

前回、エラーが出ないまま間違っているものを4件並べました。
その翌日に、5件目が出ています。

自作アプリ「Hubpin」に、パスワードを入力しなくても中を見られるデモログインを付けました。
動かしたら、Supabase からこう返ってきました。

missing email or phone

メールアドレスが無い、と言われています。
コードには渡しているつもりでした。

await supabase.auth.signInWithPassword({
  email: process.env.DEMO_EMAIL!,
  password: process.env.DEMO_PASSWORD!,
})

原因にたどり着くまでに、黙っている場所を3つ通っていたことが後から分かりました。

目次

原因はキー名がずれていたことでした

.env.local に書いてあったキー名は DEMO_ACCOUNT_EMAIL でした。
コードが読んでいるのは DEMO_EMAIL です。

名前が違うので、process.env.DEMO_EMAILundefined を返します。
それがそのまま Supabase まで届いて、「メールアドレスが無い」になりました。

書き間違いとしては単純です。
気になったのは、そこに気づくまでに誰も何も言わなかったことでした。

黙っている層が3つありました

していたこと
Next.js .env.local は読んでいる。キーが無いだけなので何も言わない
TypeScript ! を付けたので型エラーが消えている
.env.example キー名がずれていても照合しない。ただのテキスト

Next.js は起動時に「.env.local を読みました」と出します。
読んだとは言いますが、中に何が入っていたかまでは言いません。

.env.example は、必要なキーの一覧をチームで共有するためのファイルです。
そこには DEMO_EMAIL と正しく書いてありました。
書いてあるだけで、手元の .env.local と突き合わせる機能はありません。

3つとも、壊れているわけではありません。それぞれの持ち場では正しい動き。
そのうえで、キー名がずれていることを見る役目だけが誰にも割り当たっていませんでした。

undefined はこの3つを素通りして、いちばん遠い Supabase の応答として出てきました。

! が実行時にしていること

3つ目の層に自分で立てたのが ! でした。

process.env.DEMO_EMAIL の型は string | undefined です。
signInWithPasswordemail: string を要求するので、そのままだと型エラーになります。
! を付けると通ります。

このとき自分は「ここには値が入っている」という意味で書いていました。
実際には、! が実行時にしていることは何もありません。
TypeScript の記号なので、JavaScript に変換された時点で消えます。

対になる ?. のほうは残ります。

foo!        // 「絶対にある」と主張する。無ければ undefined が流れていく
foo?.bar    // 「無いかもしれない」と認める。無ければそこで undefined を返す

同じくらいの手数で書ける記号ですが、片方は消えて、片方は実行時にも効きます。
自分が書いていたのは、消えるほう。

見つけ方は1行でした

環境変数が入っているかを確かめるとき、値をそのまま出すのは避けたいところです。
スクリーンショットを撮りながら作業しているので、画像に残ります。

長さだけ出すと、必要な情報がそろいました。

console.log('DEMO_EMAIL length:', process.env.DEMO_EMAIL?.length)
  • undefined … キーが無い
  • 0 … キーはあるが値が空
  • 18 … 入っている

Boolean() で確かめると、この3つが2つに潰れます。
そして「キーが無い」と「値が空」は、Supabase 側では同じ missing email or phone になります。
手前で区別できる形にしておかないと、同じ画面を見ながら別の原因を探すことになります。

遠回りした理由は、自分で書いたエラーメッセージでした

最初に書いていたエラー処理がこれでした。

if (error) return { message: 'デモにログインできませんでした' }

ユーザーに見せる文言としては、これでいいと思っています。
ログインの失敗理由を細かく出すのは、そもそも避けたい設計です。

ただ、この方針をログにまで広げていたのが問題でした。
Supabase が何を言っているかが、画面にもターミナルにも出ていません。

1行足しました。

if (error) {
  console.error('Demo login failed:', error.message)
  return { message: 'デモにログインできませんでした' }
}

missing email or phone が出てきて、そこから解決まで2分でした。

おわりに

! を「ここには値が入っている」という意味で書いていたのですが、
実際に宣言していたのは「確認しないでいい」のほうでした。
主張しただけで、確かめてはいません。

黙る層が重なると、症状はいちばん遠い場所に出ます。
今回は3層離れていたので、手元のコードを何度見ても原因が写っていませんでした。

先に見るべきだったのは、自分が書いた文言ではなく相手の言い分でした。
console.error の1行が、3層ぶんの距離を一気に縮めています。

次回は、この ! と同じことを、型注釈でもやっていた話を書きます。

目次