Error Handling and Typed Queries
ไอเดียในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียในหนึ่งประโยค”ทุก call ของ supabase-js resolve เป็น { data, error } แทนที่จะ throw ดังนั้น pattern มาตรฐานคือเช็ก error ก่อน — พอเช็กแล้ว TypeScript จะ narrow type ของ data ให้เอง และถ้า error เป็น PostgrestError ควร branch ตาม code ที่ stable แทนที่จะดู message ที่เป็นข้อความอ่านง่าย
เช็ก error ก่อนเชื่อ data
หัวข้อที่มีชื่อว่า “เช็ก error ก่อนเชื่อ data”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 แน่นอน
shape ของ PostgrestError
หัวข้อที่มีชื่อว่า “shape ของ PostgrestError”ตอน error มีค่า จะเป็น PostgrestError ที่มี field น่ารู้สี่ตัว
message— คำอธิบายที่คนอ่านเข้าใจได้ว่าเกิดอะไรขึ้นcode— code ที่ stable ไม่ว่าจะเป็น Postgres error code (เช่น23505สำหรับ unique-constraint violation) หรือ code เฉพาะของ PostgRESTdetails— 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.'); }}Generated type จับ query ที่ผิดได้ก่อนรันจริง
หัวข้อที่มีชื่อว่า “Generated type จับ query ที่ผิดได้ก่อนรันจริง”เอา 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"]