CRM में बीस कॉन्टैक्ट्स को आर्काइव किया जाना था, लेकिन UI पर सफलता का हरा बैज चमक उठा और ऑपरेशन ने बिना कुछ किए चुपचाप काम कर दिया। एक TypeScript हेल्पर, जो Supabase-JS म्यूटेशन में error फ़ील्ड को अनिवार्य बनाता है, अब डेवलपर्स को उस विफलता को छिपाने के बजाय उसका सामना करने के लिए मजबूर करता है।

Supabase-JS का रिटर्न शेप एक जाल क्यों है

Supabase का JavaScript क्लाइंट (@supabase/supabase-js) डेटाबेस राइट (write) रिजेक्ट होने पर कोई एक्सेप्शन (exception) नहीं फेंकता है। इसके बजाय, यह { data, error } ऑब्जेक्ट के साथ प्रॉमिस (promise) को रिज़ॉल्व करता है। यदि कोई रो-लेवल सिक्योरिटी (RLS) पॉलिसी, यूनिक-की उल्लंघन (unique-key violation), या कोई अन्य बाधा क्वेरी को रोकती है, तो data null के रूप में आता है और error में डेटाबेस का मैसेज होता है। क्लाइंट यह मान लेता है कि कॉलर error की जांच करेगा; यह कॉल को कभी भी बीच में नहीं रोकता।

व्यवहार में, कई कोडबेस इस कॉल को 'फायर-एंड-फॉरगेट' (fire-and-forget) ऑपरेशन की तरह मानते हैं:

await supabase
  .from('contacts')
  .update({ statut: 'ancien', archived_at: now })
  .eq('id', id)

return { ok: true }

जब update को RLS नियम द्वारा रोका जाता है, तब भी प्रॉमिस रिज़ॉल्व हो जाता है। क्योंकि डेवलपर कभी भी { error } को डिस्ट्रक्चर (destructure) नहीं करता, इसलिए विफलता अदृश्य रहती है। फंक्शन { ok: true } रिटर्न करता है, UI सफलता दिखाता है, और डेटा अपरिवर्तित रहता है। कोई लॉग नहीं दिखता, कोई Sentry अलर्ट नहीं आता, और बग दिनों तक बना रह सकता है।

एक लिंटर (linter) काफी नहीं है

स्टैटिक एनालिसिस टूल्स यह चेतावनी दे सकते हैं कि error प्रॉपर्टी को अनदेखा किया गया है, लेकिन वे रनटाइम कॉन्ट्रैक्ट (runtime contract) को लागू नहीं कर सकते। एक डेवलपर अभी भी const _ = await … लिख सकता है और चेतावनी को शांत कर सकता है, या वे नियम को दबाने के लिए एक कमेंट जोड़ सकते हैं। मूल समस्या यह है कि टाइप सिस्टम error का उल्लेख किए बिना कॉल को सफल होने की अनुमति देता है।

mutate() हेल्पर: एरर हैंडलिंग को अनिवार्य बनाना

लेखक ने mutate() नामक एक छोटा रैपर बनाया है जो रिटर्न टाइप के आकार को बदल देता है। { data, error } के बजाय, हेल्पर एक टुपल (tuple) [data, error] रिटर्न करता है जहाँ error एक अनिवार्य फ़ील्ड है। TypeScript फिर उस किसी भी कॉल को कंपाइल करने से मना कर देता है जो दूसरे एलिमेंट को छोड़ देता है।

async function mutate<T>(promise: Promise<{ data: T | null; error: any }>) {
  const { data, error } = await promise
  // Send error to Sentry immediately
  if (error) Sentry.captureException(error)
  return [data, error] as const
}

उपयोग स्पष्ट हो जाता है:

const [result, err] = await mutate(
  supabase
    .from('contacts')
    .update({ statut: 'ancien', archived_at: now })
    .eq('id', id)
)

if (err) {
  // Handle or rethrow
  return { ok: false, message: err.message }
}
return { ok: true, data: result }

यदि कोई डेवलपर err को कैप्चर करना भूल जाता है, तो TypeScript एक एरर देता है: “Tuple type [T, any] of length 2 has no element at index 1।” जब तक एरर को ठीक नहीं किया जाता, कोड कंपाइल नहीं होगा। हेल्पर Sentry इंस्ट्रुमेंटेशन (instrumentation) भी इंजेक्ट करता है, जिससे यह सुनिश्चित होता है कि हर डेटाबेस रिजेक्शन लॉग किया जाए, भले ही कॉलर बाद में उसे अनदेखा कर दे।

क्या दांव पर है

  • डेटा अखंडता (Data integrity) – साइलेंट फेलियर (silent failures) प्रोडक्शन में अमान्य स्टेट को आने देते हैं। एक आर्काइव किया गया कॉन्टैक्ट जो सक्रिय सूची से कभी नहीं हटा, वह डाउनस्ट्रीम रिपोर्टों को गलत कर सकता है।
  • उपयोगकर्ता का भरोसा – एक ऐसा UI जो सफलता का दावा करता है जबकि कुछ भी नहीं बदला, विश्वास को कम करता है। ग्राहक "आर्काइव" देखते हैं लेकिन फिर भी सर्च में कॉन्टैक्ट पाते हैं।
  • डेवलपर का समय – काल्पनिक बग्स (phantom bugs) का पीछा करने में घंटों बर्बाद होते हैं। स्पष्ट एरर हैंडलिंग विफलता के बिंदु पर ही समस्या को सामने लाती है, जिससे डिबगिंग का समय कम हो जाता है।
  • परिचालन लागत (Operational cost) – साइलेंट डेटा लॉस की घटना की लागत की तुलना में कोड की कुछ अतिरिक्त लाइनें और एक छोटा रैपर जोड़ना नगण्य है।

समझौता (The trade-off)

हेल्पर वर्बोसिटी (verbosity) बढ़ाता है: अब हर म्यूटेशन कॉल एक टुपल रिटर्न करती है, और कॉलर को if (err) ब्लॉक लिखना होगा। कुछ टीमें इसे 'बॉयलरप्लेट नॉइज़' (boilerplate noise) मान सकती हैं, खासकर सरल CRUD कार्यों के लिए जहाँ वे सफलता की अपेक्षा करते हैं। इसका जवाबी तर्क यह है कि अतिरिक्त कोड एक सुरक्षा कवच है, कोई वैकल्पिक फीचर नहीं। उन वातावरणों में जहाँ डेटा की सटीकता सर्वोपरि है—जैसे CRM सिस्टम, फाइनेंस, हेल्थ—वहाँ इस चेक को अनिवार्य बनाना खुद को सार्थक साबित करता है।

आगे क्या

लेखक तीसरे भाग का वादा करते हैं जो बिना किसी स्पष्टीकरण के शून्य पंक्तियाँ (zero rows) लौटाने वाली RLS नीतियों की जांच करेगा। वह पैटर्न, साइलेंट-एरर केस की तरह ही, एक स्पष्ट रूप से सफल क्वेरी के पीछे विफलताओं को छिपाता है। साथ मिलकर, इस श्रृंखला का उद्देश्य Supabase के API के "अफवाह" (rumor) वाले पहलू को उजागर करना और डेवलपर्स को तथ्यों की मांग करने के लिए ठोस उपकरण देना है।

Takeaway: Supabase-JS का डिज़ाइन एक विफल राइट (write) को सफलता जैसा दिखने देता है, जब तक कि आप error फ़ील्ड को पढ़ना याद न रखें। कॉल को TypeScript-प्रवर्तित हेल्पर में लपेटकर, जो error को अनिवार्य बनाता है और इसे Sentry में लॉग करता है, आप साइलेंट फेलियर को दृश्य और कार्रवाई योग्य (actionable) घटनाओं में बदल देते हैं। कोड के आकार में मामूली वृद्धि आपको डेटा विश्वसनीयता और उपयोगकर्ता का भरोसा दिलाती है—दो ऐसी चीजें जो कोई भी साइलेंट सफलता कभी नहीं दे सकती।