قرار بود بیست مخاطب در یک CRM آرشیو شوند، اما رابط کاربری (UI) یک نشانگر موفقیت سبز رنگ را نمایش داد و عملیات بدون هیچ اثری انجام شد. یک ابزار کمکی (helper) در TypeScript که فیلد error را در تغییرات (mutations) Supabase-JS اجباری می‌کند، اکنون توسعه‌دهندگان را مجبور می‌کند تا به جای پنهان کردن آن شکست، با آن روبرو شوند.

چرا ساختار بازگشتی Supabase-JS یک تله است

کلاینت جاوااسکریپت Supabase (@supabase/supabase-js) هنگام رد شدن یک عملیات نوشتن در پایگاه داده، استثنا (exception) پرتاب نمی‌کند. در عوض، وعده (promise) را با یک شیء { data, error } حل (resolve) می‌کند. اگر یک سیاست امنیت سطح ردیف (RLS)، نقض کلید یکتا (unique-key violation) یا هر محدودیت دیگری مانع کوئری شود، data به صورت null بازمی‌گردد و error حاوی پیام پایگاه داده است. کلاینت فرض می‌کند که فراخواننده (caller) فیلد error را بررسی خواهد کرد؛ کلاینت هرگز فراخوانی را متوقف نمی‌کند.

در عمل، بسیاری از پایگاه‌های کد (codebases) با این فراخوانی مانند یک عملیات «بزن و فراموش کن» (fire-and-forget) برخورد می‌کنند:

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

return { ok: true }

وقتی update توسط یک قانون RLS مسدود شود، promise همچنان resolve می‌شود. از آنجایی که توسعه‌دهنده هرگز { error } را از ساختار خارج (destructure) نمی‌کند، شکست نامرئی باقی می‌ماند. تابع { ok: true } را برمی‌گرداند، UI موفقیت را نشان می‌دهد و داده‌ها بدون تغییر می‌مانند. هیچ لاگی ثبت نمی‌شود، هیچ هشدار Sentry ارسال نمی‌شود و این باگ می‌تواند روزها پنهان بماند.

یک لینتر (linter) کافی نیست

ابزارهای تحلیل استاتیک می‌توانند زمانی که ویژگی error نادیده گرفته می‌شود هشدار دهند، اما نمی‌توانند یک قرارداد زمان اجرا (runtime contract) را اعمال کنند. یک توسعه‌دهنده همچنان می‌تواند بنویسد const _ = await … و هشدار را ساکت کند، یا می‌تواند کامنتی برای نادیده گرفتن قانون اضافه کند. مشکل اصلی این است که سیستم تایپ اجازه می‌دهد یک فراخوانی بدون ذکر error با موفقیت انجام شود.

ابزار کمکی mutate(): اجباری کردن مدیریت خطا

نویسنده یک پوشش‌دهنده (wrapper) کوچک به نام 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.” کد تا زمانی که خطا برطرف نشود، کامپایل نخواهد شد. این ابزار همچنین ابزارگذاری (instrumentation) Sentry را تزریق می‌کند و تضمین می‌کند که هر رد شدن در پایگاه داده، حتی اگر فراخواننده بعداً آن را نادیده بگیرد، ثبت شود.

آنچه در خطر است

  • یکپارچگی داده‌ها – شکست‌های خاموش اجازه می‌دهند وضعیت‌های نامعتبر به محیط تولید (production) نفوذ کنند. یک مخاطب آرشیو شده که هرگز از لیست فعال خارج نشده است، می‌تواند باعث اشتباه در گزارش‌های بعدی شود.
  • اعتماد کاربر – رابط کاربری که ادعای موفقیت می‌کند در حالی که هیچ تغییری ایجاد نشده، اعتماد را از بین می‌برد. مشتریان کلمه «archived» را می‌بینند اما همچنان مخاطب را در جستجوها پیدا می‌کنند.
  • زمان توسعه‌دهنده – تعقیب باگ‌های خیالی ساعت‌ها وقت می‌گیرد. مدیریت صریح خطا، مشکل را در نقطه وقوع آشکار می‌کند و چرخه عیب‌یابی را کوتاه می‌کند.
  • هزینه عملیاتی – اضافه کردن چند خط کد اضافی و یک پوشش‌دهنده کوچک در مقایسه با هزینه یک حادثه از دست رفتن خاموش داده‌ها، ناچیز است.

موازنه (Trade-off)

این ابزار کمکی باعث طولانی‌تر شدن کد (verbosity) می‌شود: هر فراخوانی mutation اکنون یک tuple برمی‌گرداند و فراخوانندگان باید یک بلوک if (err) بنویسند. برخی تیم‌ها ممکن است این را به عنوان کد اضافی (boilerplate) و مزاحم ببینند، به خصوص برای عملیات‌های ساده CRUD که انتظار موفقیت دارند. استدلال متقابل این است که این کد اضافی یک محافظ است، نه یک ویژگی اختیاری. در محیط‌هایی که صحت داده‌ها حیاتی است — مانند سیستم‌های CRM، امور مالی، سلامت — اجبار به بررسی خطا، ارزش خود را ثابت می‌کند.

گام بعدی

نویسنده وعده بخش سوم را داده است که سیاست‌های RLS را بررسی می‌کند که بدون هیچ توضیحی، صفر ردیف برمی‌گردانند. آن الگو، مانند مورد خطای خاموش، شکست‌ها را پشت یک کوئری ظاهراً موفق پنهان می‌کند. این مجموعه در کنار هم قصد دارد جنبه «شایعه‌ساز» API مربوط به Supabase را افشا کند و ابزارهای ملموسی را در اختیار توسعه‌دهندگان قرار دهد تا بتوانند حقایق را مطالبه کنند.

نکته کلیدی: طراحی Supabase-JS اجازه می‌دهد یک نوشتنِ ناموفق، شبیه به یک موفقیت به نظر برسد، مگر اینکه به یاد داشته باشید فیلد error را بخوانید. با قرار دادن فراخوانی‌ها در یک ابزار کمکی که توسط TypeScript اجباری شده و error را الزامی کرده و آن را در Sentry ثبت می‌کند، شما شکست‌های خاموش را به رویدادهای قابل مشاهده و قابل اقدام تبدیل می‌کنید. افزایش اندک در حجم کد، قابلیت اطمینان داده‌ها و اعتماد کاربر را برای شما به ارمغان می‌آورد — دو چیزی که هیچ موفقیت خاموشی هرگز نمی‌تواند ارائه دهد.