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

Partial Results

บทเรียนก่อนจบไว้ที่ field errors คือ resolver throw, field กลายเป็น null แล้วมี error entry อธิบายเหตุ แต่ยังมีกฎละเอียดอ่อนซ่อนอยู่ตรงนี้ ถ้า field ที่ throw ประกาศเป็น non-null ด้วย ! ล่ะ? field แบบ non-null เป็น null ไม่ได้เลย แล้ว server จะใส่อะไรลงไปตรงนั้น? คำตอบคือ error propagation หนึ่งในพฤติกรรม runtime ที่สำคัญที่สุดของ GraphQL

เมื่อ field เกิด error server จะพยายามตั้งค่าตรงนั้นเป็น null ก่อน ถ้า schema ยอมให้เป็น null ได้ คือ field นั้น nullable ทุกอย่างก็จบตรงนั้น server บันทึก error แล้วเดินหน้าต่อ ความล้มเหลวอยู่เฉพาะที่ และ field พี่น้องยัง resolve ต่อได้ตามปกติ

แต่ถ้า field เป็น non-null การใส่ null ตรงนั้นผิดกฎ server จึงปล่อยไว้เฉย ๆ ไม่ได้ error เลยลอยขึ้นไปหา parent ถ้า parent เป็น nullable ตัว parent จะกลายเป็น null แล้วการแพร่หยุดตรงนั้น แต่ถ้า parent เป็น non-null ด้วย error ก็ปีนต่อขึ้นไปอีก ไล่ขึ้นไปเรื่อย ๆ จนเจอ ancestor ที่เป็น nullable ตัวใกล้ที่สุด และถ้าไล่ขึ้นไปจนถึง root แล้วไม่เจอ field ที่ nullable เลย ตัว data ทั้งก้อนจะกลายเป็น null

flowchart TD
  Root["Query (root)"]
  Profile["profile: Profile (nullable)"]
  Account["account: Account! (non-null)"]
  Name["legalName: String! (non-null, throws)"]
  Root --> Profile
  Profile --> Account
  Account --> Name
  Name -->|"null illegal, bubble up"| Account
  Account -->|"non-null, keep bubbling"| Profile
  Profile -->|"nullable: absorbs error, becomes null"| Root
error ของ field แบบ non-null ลอยขึ้นไปจนกว่าจะเจอ parent ที่ nullable

ไดอะแกรมไล่ให้ดูทีละขั้น leaf แบบ non-null ตัวหนึ่ง throw ส่วน parent ที่เป็น non-null ก็ถือ null ไว้ไม่ได้ error จึงปีนต่อขึ้นไปหา field nullable ตัวแรกที่อยู่เหนือขึ้นไป field ตัวนั้นทำหน้าที่เป็นกำแพงไฟ ดูดซับ error แล้วปกป้อง response ส่วนที่เหลือไว้

กฎนี้ทำให้ ! กลายเป็นการตัดสินใจเรื่อง blast radius ไปเลย field แบบ non-null เป็นทั้งคำสัญญาและภาระในตัวเดียวกัน พอพังขึ้นมาก็ลาก non-null ancestor ล่มตามไปด้วย ส่วน field แบบ nullable เป็นวาล์วนิรภัย กักความล้มเหลวไว้กับตัวเอง ปล่อยให้ field พี่น้องรอด แล้วให้ partial data กลับไป

ดังนั้น nullability ไม่ได้ตอบแค่ว่าค่านี้หายไปได้ไหมในทางปกติ แต่ตอบด้วยว่าความล้มเหลวจะลามไปไกลแค่ไหน ใส่ ! ให้ field ที่คุณมั่นใจว่า server ผลิตได้ทุกครั้ง และปล่อยเป็น nullable เมื่ออยากให้ค่อย ๆ เสื่อมลงอย่างนุ่มนวล ดีกว่าปล่อยให้ subtree ทั้งก้อนล่ม

ตัว runner ด้านล่างสร้าง Profile ที่มี field พี่น้องสองตัว คือ bio แบบ nullable ที่ resolver throw และ displayName แบบ non-null ที่ resolve ผ่านฉลุย เพราะ bio เป็น nullable ความล้มเหลวจึงถูกกักไว้แค่ตรงนั้น และ displayName ยังกลับมาครบ กด Run แล้วมองหา partial data ที่วางอยู่ข้าง ๆ error ตัวเดียว

JavaScript

อ่าน output เป็นเรื่องของการกักความเสียหาย ตัว bio เป็น nullable พอ resolver throw server จึงตั้งค่าเป็น null แล้วบันทึก error หนึ่งตัวพร้อม path: ["profile", "bio"] ที่สำคัญคือ displayName ไม่โดนกระทบ ยังได้ "Ada Lovelace" กลับมาเหมือนเดิม นี่แหละ partial data — response ที่ยังใช้งานได้ทั้งที่มีความล้มเหลวอยู่ข้างใน ถ้าประกาศเป็น bio: String! แทน error จะลอยขึ้นไปหา profile (nullable) แล้ว null ทั้ง profile object ไม่ใช่แค่ field เดียว

ข้อดีข้อแลกเปลี่ยน
field บางตัว fail ไม่ทำให้ query ทั้งหมด failclient ต้องจัดการ partial data + error พร้อมกัน
user ยังเห็นข้อมูลที่ fetch ได้แม้บาง field errornull propagation อาจทำให้ field ที่ non-null null ได้ถ้า parent null
resolver แต่ละตัวทำงาน independentpartial success ยาก track ใน monitoring
เหมาะกับ non-critical field เช่น recommendation, analyticsclient ที่ไม่ check error อาจแสดง incomplete data โดยไม่รู้

ไม่ระบุว่า Field ไหน Non-critical

อาการ:

  • critical field (price, stock) และ non-critical field (related product) มี nullability เดียวกัน
  • field ที่ควร fail gracefully กลับทำให้ response ทั้งหมด null
  • ทำ non-critical field เป็น nullable และออกแบบ null state ใน UI

Null Propagation ที่ไม่คาดคิด

อาการ:

  • order.payment!.status — payment เป็น non-null
  • ถ้า payment resolver fail → payment เป็น null → ทำให้ order เป็น null ทั้งหมด
  • ออกแบบ non-null อย่างระวัง — field ที่อาจ fail ควร nullable

💡 ตัวอย่างจากของจริง

Netflix:

  • หน้า home แสดง recommendation section — ถ้า recommendation service ล้ม
  • section นั้น null แต่ส่วนอื่น (hero, trending) ยังแสดงได้
  • partial result ทำให้ degraded experience ดีกว่า full error

Airbnb:

  • listing page มีหลาย section (price, reviews, host, amenities)
  • แต่ละ section resolver แยกกัน — section ที่ fail ไม่ block section อื่น
ถ้า resolver ของ field แบบ nullable throw จะเกิดอะไรขึ้นกับ field พี่น้อง?
เมื่อ field แบบ non-null เกิด error ตัว error จะแพร่ไปที่ไหน?
ถ้า field แบบ non-null เกิด error และไล่ขึ้นไปถึง root แล้วไม่เจอ field ที่ nullable เลย ผลลัพธ์จะเป็นอย่างไร?
การใส่ ! ให้ field บ่งบอกอะไรเกี่ยวกับความล้มเหลว?