Двадцять контактів мали бути архівовані в CRM, проте інтерфейс блиснув зеленим значком успіху, а операція мовчки нічого не зробила. TypeScript-хелпер, який робить поле error обов'язковим у мутаціях Supabase-JS, тепер змушує розробників стикатися з цією помилкою замість того, щоб замітати її під килим.

Чому форма результату Supabase-JS — це пастка

JavaScript-клієнт Supabase (@supabase/supabase-js) не викидає винятки (exceptions), коли запис у базу даних відхилено. Замість цього він повертає проміс із об'єктом { data, error }. Якщо політика безпеки на рівні рядків (RLS), порушення унікального ключа або будь-яке інше обмеження блокує запит, 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, проміс все одно успішно завершується (resolves). Оскільки розробник ніколи не деструктуризує { error }, помилка залишається непомітною. Функція повертає { ok: true }, інтерфейс показує успіх, а дані залишаються незмінними. Жодних логів, жодних сповіщень у Sentry, і баг може залишатися непоміченим днями.

Лінтера недостатньо

Інструменти статичного аналізу можуть попередити, якщо властивість 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, гарантуючи, що кожне відхилення бази даних буде залогуване, навіть якщо викликаючий код згодом його проігнорує.

Що стоїть на кону

  • Цілісність даних – мовчазні помилки дозволяють некоректним станам проникати у продакшн. Контакт, який мав бути архівований, але залишився в активному списку, може призвести до помилок у подальших звітах.
  • Довіра користувачів – інтерфейс, який заявляє про успіх, коли насправді нічого не змінилося, підриває довіру. Клієнти бачать статус «архівовано», але все одно знаходять контакт у пошуку.
  • Час розробника – пошук фантомних багів забирає години. Явна обробка помилок виявляє проблему безпосередньо в місці збою, скорочуючи цикл налагодження.
  • Операційні витрати – додавання кількох зайвих рядків коду та невеликої обгортки є незначним порівняно з ціною інциденту, пов'язаного з мовчазною втратою даних.

Компроміс

Хелпер додає багатослівність: кожен виклик мутації тепер повертає кортеж, і викликаючий код повинен містити блок if (err). Деякі команди можуть сприйняти це як зайвий шаблонний код (boilerplate), особливо для простих CRUD-дій, де вони очікують успіху. Контраргумент полягає в тому, що цей додатковий код є запобіжником, а не опціональною функцією. У середовищах, де правильність даних є першочерговою — CRM-системи, фінанси, медицина — примусова перевірка окупає себе.

Що далі

Автор обіцяє третю частину, у якій розглянеться ситуація, коли політики RLS повертають нуль рядків без пояснень. Цей патерн, як і випадок із мовчазною помилкою, приховує збої за виглядом успішного запиту. Разом серія має на меті викрити «недомовки» API Supabase та надати розробникам конкретні інструменти для отримання фактів.

Висновок: Дизайн Supabase-JS дозволяє невдалому запису виглядати як успішний, якщо ви не пам'ятаєте перевірити поле error. Обгортаючи виклики в хелпер, що підтримується TypeScript, робить error обов'язковим і логує його в Sentry, ви перетворюєте мовчазні помилки на видимі події, що потребують дій. Незначне збільшення обсягу коду забезпечує вам надійність даних і довіру користувачів — дві речі, які жоден «мовчазний успіх» ніколи не зможе забезпечити.