একটি CRM-এ বিশটি কন্টাক্ট আর্কাইভ করার কথা ছিল, অথচ UI-তে একটি সবুজ সাকসেস ব্যাজ দেখালেও অপারেশনটি নিঃশব্দে কিছুই করেনি। একটি TypeScript হেল্পার যা Supabase-JS মিউটেশনে error ফিল্ডটিকে বাধ্যতামূলক করে তোলে, তা এখন ডেভেলপারদের সেই ব্যর্থতা এড়িয়ে না গিয়ে সরাসরি মোকাবিলা করতে বাধ্য করে।
কেন Supabase-JS-এর রিটার্ন শেপ একটি ফাঁদ
Supabase-এর JavaScript ক্লায়েন্ট (@supabase/supabase-js) ডাটাবেস রাইট রিজেক্ট হলে কোনো এক্সেপশন (exception) থ্রো করে না। পরিবর্তে এটি { data, error } অবজেক্ট সহ প্রমিসটি রিজলভ (resolve) করে। যদি কোনো row-level security (RLS) পলিসি, একটি ইউনিক-কী ভায়োলেশন (unique-key violation), বা অন্য কোনো কনস্ট্রেইন্ট কুয়েরিকে বাধা দেয়, তবে data হিসেবে null আসে এবং error-এ ডাটাবেস মেসেজ থাকে। ক্লায়েন্ট ধরে নেয় যে কলকারী errorটি পরীক্ষা করবেন; এটি কখনোই কলটি বাতিল (abort) করে না।
বাস্তবে অনেক কোডবেস এই কলটিকে একটি '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 – নিঃশব্দ ব্যর্থতাগুলো প্রোডাকশনে ইনভ্যালিড স্টেট (invalid state) প্রবেশ করতে দেয়। একটি আর্কাইভ করা কন্টাক্ট যা কখনোই অ্যাক্টিভ লিস্ট থেকে সরেনি, তা পরবর্তী রিপোর্টগুলোকে ভুল করে তুলতে পারে।
- User trust – কোনো পরিবর্তন না হওয়া সত্ত্বেও একটি UI যদি সাফল্য দাবি করে, তবে তা ব্যবহারকারীর আস্থা কমিয়ে দেয়। গ্রাহকরা “archived” দেখেন কিন্তু সার্চ করলে কন্টাক্টটি তখনও খুঁজে পান।
- Developer time – কাল্পনিক বাগ (phantom bugs) খুঁজতে অনেক সময় নষ্ট হয়। স্পষ্ট এরর হ্যান্ডলিং ব্যর্থতার মুহূর্তেই সমস্যাটি সামনে নিয়ে আসে, যা ডিবাগিং লুপ কমিয়ে দেয়।
- Operational cost – একটি নিঃশব্দ ডাটা লস ইনসিডেন্টের খরচের তুলনায় কয়েক লাইন অতিরিক্ত কোড এবং একটি ছোট র্যাপার যোগ করা নগণ্য।
ট্রেড-অফ (The trade-off)
এই হেল্পারটি কোডকে কিছুটা দীর্ঘ (verbose) করে তোলে: প্রতিটি মিউটেশন কল এখন একটি টাপল রিটার্ন করে এবং কলকারীকে অবশ্যই একটি if (err) ব্লক লিখতে হবে। কিছু টিম এটিকে অপ্রয়োজনীয় 'boilerplate noise' হিসেবে দেখতে পারে, বিশেষ করে সাধারণ CRUD অ্যাকশনের ক্ষেত্রে যেখানে তারা সাফল্যের আশা করে। এর পাল্টা যুক্তি হলো, অতিরিক্ত কোডটি একটি সুরক্ষা কবচ, কোনো ঐচ্ছিক ফিচার নয়। যেসব পরিবেশে ডাটার সঠিকতা অত্যন্ত গুরুত্বপূর্ণ—যেমন CRM সিস্টেম, ফিন্যান্স, হেলথ—সেখানে এই চেকটি বাধ্যতামূলক করা অত্যন্ত ফলপ্রসূ।
পরবর্তী পদক্ষেপ
লেখক তৃতীয় একটি পর্বের প্রতিশ্রুতি দিয়েছেন যেখানে কোনো ব্যাখ্যা ছাড়াই শূন্য রো (zero rows) রিটার্ন করা RLS পলিসিগুলো পরীক্ষা করা হবে। সেই প্যাটার্নটি, নিঃশব্দ-এরর কেসের মতো, একটি আপাতদৃষ্টিতে সফল কুয়েরির আড়ালে ব্যর্থতা লুকিয়ে রাখে। এই সিরিজটির লক্ষ্য হলো Supabase-এর API-এর "গুজব" বা অস্পষ্ট দিকগুলো উন্মোচন করা এবং ডেভেলপারদের তথ্য নিশ্চিত করার জন্য বাস্তবসম্মত টুল প্রদান করা।
মূল কথা (Takeaway): Supabase-JS-এর ডিজাইন একটি ব্যর্থ রাইটকে সফল হিসেবে দেখাতে পারে, যদি না আপনি error ফিল্ডটি পড়ার কথা মনে রাখেন। কলগুলোকে একটি TypeScript-enforced হেল্পার দিয়ে র্যাপ করার মাধ্যমে যা error-কে বাধ্যতামূলক করে এবং Sentry-তে লগ করে, আপনি নিঃশব্দ ব্যর্থতাগুলোকে দৃশ্যমান এবং কার্যকর ইভেন্টে পরিণত করতে পারেন। কোডের সামান্য বৃদ্ধি আপনাকে ডাটা নির্ভরযোগ্যতা এবং ব্যবহারকারীর আস্থা প্রদান করে—যা কোনো নিঃশব্দ সাফল্য কখনোই দিতে পারে না।
