> For the complete documentation index, see [llms.txt](https://docs.skail.dev/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.skail.dev/construa/conceitos/retries-e-idempotencia.md).

# Retries e idempotência

O que acontece quando um command lança exceção, quando uma function falha, quantas vezes o skail tenta de novo, e por que a idempotência da operação externa continua sendo responsabilidade do seu código.

## Retry sem configurar nada

O mecanismo é a reentrega. Quando um passo falha, o skail entrega a execução de novo à sua aplicação. A function é reexecutada (replay), os commands já concluídos devolvem os resultados gravados, e o passo que falhou roda outra vez. Uma oscilação de rede entre a sua aplicação e o skail não vira falha da execução: o runtime lida com isso antes de contar uma tentativa. O `retryCount` de cada command e de cada function define quantas reentregas cada passo aceita: 5 para command, 15 para function, por padrão.

## O que acontece quando um command lança exceção

<figure><img src="https://1898946878-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsJmS5ILPG5x2x5j0WyiV%2Fuploads%2FwdKJDF0AaXGcmW6vN1yN%2Fap-falhas-retries.svg?alt=media" alt="Uma execução em entregas sucessivas. Na entrega 1, a reserva de estoque executa e a cobrança do cartão lança uma exceção de HTTP; o runtime registra a falha e devolve a execução ao skail, e a function ainda não vê nada. Nas reentregas, o replay devolve o resultado gravado da reserva e a cobrança executa de novo, até concluir ou esgotar o retryCount; na última, a exceção chega ao await e entra o seu try/catch."><figcaption></figcaption></figure>

Passo a passo, com um command `CobrarCartao` que chama o gateway de pagamento:

1. O gateway devolve erro e `CobrarCartao` lança uma exceção de HTTP.
2. O runtime registra a falha e devolve a execução ao skail. A function não vê nada ainda.
3. O skail reentrega a execução. A function roda de novo desde o início; `ReservarEstoque`, que já tinha concluído, devolve o resultado gravado sem executar. `CobrarCartao` executa de novo.
4. Isso se repete até o command concluir ou até esgotar o `retryCount` (5 tentativas por padrão).
5. Quando esgota, a exceção é lançada dentro da function, no `await CobrarCartao(...)`, como qualquer exceção em `async`/`await`. É aqui, e só aqui, que o seu `try/catch` entra.

Isso tem duas consequências práticas. A primeira é que a function não vê as tentativas intermediárias, então um laço de retry manual com `try/catch` dentro dela duplica o que o runtime já fez. Ajuste o `retryCount` do command e trate a exceção final uma vez. A segunda é que o runtime não classifica exceções em transitórias ou definitivas: qualquer exceção gera nova tentativa. Se uma falha é definitiva por natureza (cartão recusado, CPF inválido), não a represente como exceção: devolva um resultado (no exemplo abaixo, `Recusado`) e decida na function.

{% tabs %}
{% tab title="C#" %}
{% code lineNumbers="true" %}

```csharp
[SkailFunction]
public async SkailTask ProcessarPedido(Guid pedidoId)
{
    await ReservarEstoque(pedidoId);

    ResultadoCobranca resultado;
    try
    {
        resultado = await CobrarCartao(pedidoId);   // exceção só chega aqui depois de 5 tentativas
    }
    catch (HttpRequestException)
    {
        await LiberarEstoque(pedidoId);               // compensação
        await MarcarPedidoComoPendente(pedidoId);
        return;
    }

    if (resultado == ResultadoCobranca.Recusado)      // falha de negócio: não é exceção
    {
        await LiberarEstoque(pedidoId);
        return;
    }

    await ConfirmarPedido(pedidoId);
}

[SkailCommand(retryCount: 8)]
public async SkailTask<ResultadoCobranca> CobrarCartao(Guid pedidoId) { /* gateway */ }
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
{% code lineNumbers="true" %}

```typescript
import { SkailCommand, SkailFunction } from "@skail/runtime";

type ResultadoCobranca = "Aprovado" | "Recusado";

class HttpRequestError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "HttpRequestError";
  }
}

export class Pedidos {
  @SkailFunction()
  async processarPedido(pedidoId: string): Promise<void> {
    await this.reservarEstoque(pedidoId);

    let resultado: ResultadoCobranca;
    try {
      resultado = await this.cobrarCartao(pedidoId); // exceção só chega aqui depois de 5 tentativas
    } catch (error) {
      if (!(error instanceof HttpRequestError)) throw error;
      await this.liberarEstoque(pedidoId); // compensação
      await this.marcarPedidoComoPendente(pedidoId);
      return;
    }

    if (resultado === "Recusado") {
      // falha de negócio: não é exceção
      await this.liberarEstoque(pedidoId);
      return;
    }

    await this.confirmarPedido(pedidoId);
  }

  @SkailCommand({ retryCount: 8 })
  async cobrarCartao(pedidoId: string): Promise<ResultadoCobranca> {
    /* gateway */
    const resposta = await fetch("https://gateway.exemplo.com/charges", {
      method: "POST",
      headers: { "Idempotency-Key": `cobranca-${pedidoId}` },
      body: JSON.stringify({ pedidoId }),
    });
    if (!resposta.ok) throw new HttpRequestError(`Gateway respondeu ${resposta.status}`);
    return "Aprovado";
  }

  @SkailCommand()
  async reservarEstoque(pedidoId: string): Promise<void> {
    /* ... */
  }

  @SkailCommand()
  async liberarEstoque(pedidoId: string): Promise<void> {
    /* ... */
  }

  @SkailCommand()
  async marcarPedidoComoPendente(pedidoId: string): Promise<void> {
    /* ... */
  }

  @SkailCommand()
  async confirmarPedido(pedidoId: string): Promise<void> {
    /* ... */
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

{% hint style="info" %}
Quando o `retryCount` do command deve mudar: para cima em integrações sabidamente instáveis; para zero quando o efeito não pode ser repetido de forma alguma e você prefere tratar a falha na function na primeira ocorrência.
{% endhint %}

## O que acontece quando a function lança exceção

Se a exceção sai da function (você não capturou, ou relançou), a execução é tratada como falha da function e reentregue até o `retryCount` dela, 15 por padrão. Em cada reentrega os commands concluídos não rodam de novo; só o trecho que ainda não tinha passo executa. Quando as 15 se esgotam, a execução é encerrada como falha e aparece assim no Monitor, com a exceção e a linha do tempo. Depois de corrigir a causa (um dado no banco, um serviço fora), ela pode ser retomada manualmente: [Retomada manual de execuções falhas](/opere/retomada-manual-de-execucoes-falhas.md).

{% hint style="warning" %}
Um detalhe que costuma surpreender: uma exceção de command que esgotou as tentativas e não foi capturada faz a function ser reentregue 15 vezes, e a cada uma o command roda de novo suas 5 tentativas. Se você não quer isso, capture na function e decida.
{% endhint %}

## O efeito é "no mínimo uma vez"

O que o skail garante: depois que um command grava o resultado no histórico, ele nunca mais executa para aquela execução. O que o skail não pode garantir: que a operação externa aconteceu exatamente uma vez. Se o command chamou o gateway, o gateway cobrou, e o processo caiu antes de o runtime gravar o resultado, a próxima tentativa chama o gateway de novo.

A janela é pequena, mas existe em qualquer sistema distribuído, e o jeito de fechá-la é o mesmo em todos: idempotência no lado de fora, com uma chave de negócio. Passe o id do pedido, da fatura, da tentativa como chave de idempotência na chamada externa; o gateway, o ERP ou o seu próprio banco reconhecem a repetição e não duplicam o efeito. [Como escrever commands idempotentes](/construa/como-fazer/escrever-commands-idempotentes.md) mostra o padrão em código.

Dizer isso ao seu time é ganho, não perda: quem já operou filas sabe que "exatamente uma vez" no efeito não existe, e desconfia de quem promete.

## Resumo das regras

* Command: qualquer exceção gera nova tentativa até `retryCount`; a function só vê a exceção depois disso. Falha de negócio é resultado, não exceção.
* Function: exceção não capturada gera reentrega até o seu `retryCount`; commands concluídos não repetem; ao esgotar, a execução fica como falha no Monitor e pode ser retomada.
* Operação externa: no mínimo uma vez; idempotência por chave de negócio é sua.

## Próximos passos

[Como tratar erros e compensar](/construa/como-fazer/tratar-erros-e-desfazer-passos.md) tem o padrão completo com Saga. [Como escrever commands idempotentes](/construa/como-fazer/escrever-commands-idempotentes.md) fecha a janela do "no mínimo uma vez". A [referência do command](/referencia/sdk-csharp/skailcommand.md) descreve o parâmetro `retryCount`.
