ข้ามไปยังเนื้อหา

Error Handling and Typed Queries

ทุก call ของ supabase-js resolve เป็น { data, error } แทนที่จะ throw ดังนั้น pattern มาตรฐานคือเช็ก error ก่อน — พอเช็กแล้ว TypeScript จะ narrow type ของ data ให้เอง และถ้า error เป็น PostgrestError ควร branch ตาม code ที่ stable แทนที่จะดู message ที่เป็นข้อความอ่านง่าย

pattern นี้เห็นมาแล้วในบทก่อน ตอนนี้จะพูดให้เป็นทางการ ไม่มี call ไหนของ supabase-js throw ตอน request fail จะ resolve ตามปกติเสมอ โดยผลลัพธ์ถูกแบ่งเป็น data กับ error

const { data, error } = await supabase
.from('profiles')
.select('id, username')
.eq('id', userId)
.single();
if (error) {
console.error(error.message);
} else {
// TypeScript now knows data is not null here
console.log(data.username);
}

เพราะไม่มีอะไรถูก throw request ที่ fail แล้วลืมเช็ก ก็แค่ดูเหมือน data เป็น null เฉย ๆ ไม่มี stack trace มาบังคับให้สังเกต การเช็ก error ก่อนไม่ใช่แค่ best practice แต่เป็นวิธีเดียวที่บอกได้ชัดว่าสำเร็จหรือ fail และยังเป็นสิ่งที่ทำให้ TypeScript narrow type ของ data ได้ด้วย ใน branch else (หรือหลัง early return ตอน error) data จะถูกรู้ว่าไม่ใช่ null แน่นอน

ตอน error มีค่า จะเป็น PostgrestError ที่มี field น่ารู้สี่ตัว

  • message — คำอธิบายที่คนอ่านเข้าใจได้ว่าเกิดอะไรขึ้น
  • code — code ที่ stable ไม่ว่าจะเป็น Postgres error code (เช่น 23505 สำหรับ unique-constraint violation) หรือ code เฉพาะของ PostgREST
  • details — context เพิ่มเติมจาก Postgres ถ้ามี
  • hint — คำแนะนำวิธีแก้ ถ้า Postgres หรือ PostgREST เสนอมาให้ได้

กฎที่ใช้จริงคือ match บน error.code สำหรับการตัดสินใจแบบ programmatic ไม่ใช่ error.message ข้อความ message มีไว้ให้คนอ่าน และอาจเปลี่ยนคำระหว่าง version ส่วน code คือสัญญาที่ stable ที่ branch ตามได้อย่างปลอดภัย

const { error } = await supabase
.from('profiles')
.insert({ id: userId, username: 'ada' });
if (error) {
if (error.code === '23505') {
// Postgres unique_violation — a stable code, unlike the message text
showError('That username is taken.');
} else {
showError('Something went wrong, please try again.');
}
}

เอา Database type จากบทก่อนไปใส่ให้ createClient<Database>() แปลว่าทุก .select(), .insert() และ .update() จะถูกเช็กกับ schema จริงตอน compile time

import { createClient } from '@supabase/supabase-js';
import type { Database } from './database.types';
const supabase = createClient<Database>(
process.env.SUPABASE_URL!,
process.env.SUPABASE_ANON_KEY!
);
type ProfileInsert = Database['public']['Tables']['profiles']['Insert'];
async function createProfile(profile: ProfileInsert) {
const { data, error } = await supabase
.from('profiles')
.insert(profile)
.select()
.single();
if (error) {
if (error.code === '23505') {
throw new Error('That username is taken.');
}
throw error;
}
return data;
}

พอตั้งค่าแบบนี้แล้ว การ select column ที่ไม่มีอยู่จริงใน profiles หรือเรียก createProfile ด้วย object ที่ขาด field ที่จำเป็นจาก Insert จะกลายเป็น TypeScript error ตอน build — รู้ตั้งแต่ตอนเขียนโค้ด ไม่ใช่มารู้จาก request ที่ fail ตอน production

flowchart LR
  call[".from('profiles').insert(profile)"] --> result["{ data, error }"]
  result --> check{"error present?"}
  check -->|"yes"| code{"error.code"}
  code -->|"23505"| unique["Show: username already taken"]
  code -->|"other"| generic["Show a generic failure message"]
  check -->|"no"| typed["Use typed data safely"]
Branch on error, and specifically on error.code, before trusting data
ผลลัพธ์ query ของ supabase-js มี shape แบบไหน และรู้ได้อย่างไรว่า fail
ทำไม match บน error.code ถึง robust กว่า match บน error.message
การเอา generated Database type ไปใส่ createClient ให้ประโยชน์อะไรตอน compile-time กับ insert และ update
Postgres error code ที่มักเห็นตอน insert ละเมิด unique constraint คืออะไร