Vinte contatos deveriam ter sido arquivados em um CRM, mas a interface exibiu um selo de sucesso verde e a operação não fez nada silenciosamente. Um helper em TypeScript que torna o campo error obrigatório em mutações do Supabase-JS agora força os desenvolvedores a confrontar essa falha em vez de escondê-la debaixo do tapete.

Por que o formato de retorno do Supabase-JS é uma armadilha

O cliente JavaScript do Supabase (@supabase/supabase-js) não lança exceções quando uma escrita no banco de dados é rejeitada. Em vez disso, ele resolve a promise com um objeto { data, error }. Se uma política de segurança em nível de linha (RLS), uma violação de chave única ou qualquer outra restrição bloquear a consulta, o data retornará como null e o error conterá a mensagem do banco de dados. O cliente assume que quem faz a chamada irá inspecionar o error; ele nunca aborta a chamada.

Na prática, muitas bases de código tratam a chamada como uma operação de "disparar e esquecer":

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

return { ok: true }

Quando o update é bloqueado por uma regra de RLS, a promise ainda é resolvida. Como o desenvolvedor nunca desestrutura { error }, a falha torna-se invisível. A função retorna { ok: true }, a interface mostra sucesso e os dados permanecem inalterados. Nenhum log aparece, nenhum alerta do Sentry é disparado e o bug pode permanecer por dias.

Um linter não é suficiente

Ferramentas de análise estática podem avisar quando uma propriedade error é ignorada, mas elas não podem impor um contrato de tempo de execução. Um desenvolvedor ainda pode escrever const _ = await … e silenciar o aviso, ou pode adicionar um comentário para suprimir a regra. O problema subjacente é que o sistema de tipos permite que uma chamada tenha sucesso sem nunca mencionar o error.

O helper mutate(): tornando o tratamento de erros obrigatório

O autor construiu um pequeno wrapper chamado mutate() que altera o formato do tipo de retorno. Em vez de { data, error }, o helper retorna uma tupla [data, error] onde error é um campo obrigatório. O TypeScript, então, recusa-se a compilar qualquer chamada que descarte o segundo elemento.

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
}

O uso torna-se explícito:

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 }

Se um desenvolvedor esquecer de capturar o err, o TypeScript emitirá um erro: “Tuple type [T, any] of length 2 has no element at index 1.” O código não compilará até que o erro seja tratado. O helper também injeta instrumentação do Sentry, garantindo que cada rejeição do banco de dados seja registrada, mesmo que quem faça a chamada a ignore posteriormente.

O que está em jogo

  • Integridade dos dados – Falhas silenciosas permitem que estados inválidos se infiltrem na produção. Um contato arquivado que nunca saiu da lista ativa pode fazer com que relatórios subsequentes estejam incorretos.
  • Confiança do usuário – Uma interface que afirma sucesso enquanto nada mudou corrói a confiança. Os clientes veem “arquivado”, mas ainda encontram o contato em buscas.
  • Tempo do desenvolvedor – Caçar bugs fantasmas consome horas. O tratamento explícito de erros traz o problema à tona no ponto da falha, encurtando o ciclo de depuração.
  • Custo operacional – Adicionar algumas linhas extras de código e um pequeno wrapper é insignificante comparado ao custo de um incidente de perda silenciosa de dados.

O trade-off

O helper adiciona verbosidade: cada chamada de mutação agora retorna uma tupla, e quem chama deve escrever um bloco if (err). Algumas equipes podem ver isso como ruído de boilerplate, especialmente para ações simples de CRUD onde esperam sucesso. O contra-argumento é que o código extra é uma salvaguarda, não um recurso opcional. Em ambientes onde a correção dos dados é primordial — sistemas de CRM, finanças, saúde — forçar a verificação se paga.

O que vem a seguir

O autor promete uma terceira parte que examina políticas de RLS que retornam zero linhas sem explicação. Esse padrão, assim como o caso do erro silencioso, esconde falhas por trás de uma consulta aparentemente bem-sucedida. Juntas, as partes da série visam expor o lado de "rumores" da API do Supabase e dar aos desenvolvedores ferramentas concretas para exigir fatos.

Resumo: O design do Supabase-JS permite que uma escrita falha pareça um sucesso, a menos que você se lembre de ler o campo error. Ao envolver as chamadas em um helper reforçado pelo TypeScript que torna o error obrigatório e o registra no Sentry, você transforma falhas silenciosas em eventos visíveis e acionáveis. O modesto aumento no tamanho do código garante confiabilidade dos dados e confiança do usuário — duas coisas que nenhum sucesso silencioso pode jamais entregar.