Pular para o conteúdo principal
Página não listada
Esta página não está listada. Mecanismos de busca não irão indexá-la, e somente usuários que possuam o link direto poderão acessá-la

Tratamento de Erros de Negócio

O Kikwiflow distingue explicitamente dois tipos de exceção lançada por um TaskHandler, com tratamento completamente diferente: erros técnicos (bugs, falhas de infraestrutura) seguem o caminho de retry e incidentes; erros de negócio esperados — uma validação que falhou, uma regra que rejeitou o caso — podem ser modelados como parte do próprio fluxo do processo, através de ProcessErrorException e nós BOUNDARY_ERROR_HANDLER.

ProcessErrorException: sinalizando um erro de negócio

public class ProcessErrorException extends RuntimeException {
private final String errorCode;

public ProcessErrorException(String errorCode, String message) { super(message); this.errorCode = errorCode; }
public ProcessErrorException(String errorCode) { super("Business process error code: " + errorCode); this.errorCode = errorCode; }

public String getErrorCode() { return errorCode; }
}

Qualquer TaskHandler pode lançar essa exceção para sinalizar que a execução daquele nó terminou em um estado de erro esperado pelo modelo do processo, identificado por um errorCode de negócio (não uma classe Java):

@Component("validateDocumentTaskHandler")
public class ValidateDocumentTaskHandler implements TaskHandler {
@Override
public void handle(ExecutionContext execution) {
String taxId = execution.getVariable("taxId").value().toString();
if (!isValidTaxId(taxId)) {
throw new ProcessErrorException("DOCUMENTO_INVALIDO", "CPF/CNPJ fora do padrão esperado");
}
// ...
}
}

BOUNDARY_ERROR_HANDLER: capturando o erro no fluxo

Um ErrorHandlerDefinition é um nó de borda (BoundaryEventDefinition), anexado a um EXECUTABLE_TASK ou EXTERNAL_TASK via attachedToRef/boundaryEventIds, exatamente como um timer de borda:

{
"id": "error-handler-documento-invalido",
"type": "BOUNDARY_ERROR_HANDLER",
"attachedToRef": "validar-documento-task",
"errorCode": "DOCUMENTO_INVALIDO",
"outgoing": [ { "targetNodeId": "solicitar-reenvio-documento" } ]
}

Quando ProcessExecutionManager captura uma ProcessErrorException lançada durante a execução de um nó, ele delega ao Navigator a busca por um handler correspondente antes de considerar aquilo uma falha técnica:

} catch (ProcessErrorException processError) {
Optional<ErrorHandlerDefinition> boundary =
navigator.findMatchingErrorHandler(currentNode, processDefinition, processError.getErrorCode());

if (boundary.isPresent()) {
status = NodeExecutionStatus.INTERRUPTED;
ErrorHandlerDefinition handler = boundary.get();
List<FlowNodeDefinition> nextNodes = handler.outgoing().stream()
.map(seq -> processDefinition.flowNodes().get(seq.targetNodeId())).toList();
List<String> nextNodeKeys = handler.outgoing().stream().map(seq -> seq.targetNodeId()).toList();
continuation = new Continuation(nextNodes, nextNodeKeys, Boolean.TRUE.equals(handler.commitAfter()));
} else {
status = NodeExecutionStatus.ERROR;
caughtException = processError; // vira uma falha técnica normal
}
}

Só um EXECUTABLE_TASK ou EXTERNAL_TASK com boundaryEventIds apontando para o ErrorHandlerDefinition entra nessa busca — findMatchingErrorHandler resolve os handlers exclusivamente por boundaryEventIds do nó que falhou (não por attachedToRef), então um BOUNDARY_ERROR_HANDLER só é considerado se o nó pai também o listar em boundaryEventIds.

Regra de casamento do errorCode

Navigator.findMatchingErrorHandler filtra os boundary events do nó que falhou pelos que são ErrorHandlerDefinition, e escolhe o primeiro que satisfaça:

.filter(handler -> handler.errorCode() == null || handler.errorCode().equals(errorCode))

Ou seja: um ErrorHandlerDefinition com errorCode: null funciona como um handler curinga, capturando qualquer ProcessErrorException lançada por aquele nó, independentemente do código específico. Declare um handler com errorCode explícito quando o processo precisa reagir de forma diferente a códigos de erro distintos; declare um handler sem errorCode como fallback genérico.

O que acontece quando não há handler correspondente

Se nenhum ErrorHandlerDefinition casar com o errorCode lançado, a ProcessErrorException não é engolida silenciosamente — ela segue para o FailureHandler exatamente como qualquer outra exceção não tratada, mas com uma diferença importante: o FailureHandler a marca como isUnhandledBusinessError = true e força a abertura de um incidente (tipo UNHANDLED_BUSINESS_ERROR), pulando a lógica normal de retry — insistir em reexecutar o mesmo TaskHandler não vai fazer uma regra de negócio que rejeitou o caso mudar de ideia. Ver Retry e Resiliência.

Interrupção do nó pai

Assim como um timer de borda interruptivo, um ErrorHandlerDefinition acionado interrompe o nó ao qual está anexado — o status registrado no evento FLOW_NODE_FINISHED (quando observabilidade está habilitada) é NodeExecutionStatus.INTERRUPTED, não SUCCESS nem ERROR. Isso permite que dashboards de observabilidade distingam claramente "o nó terminou com sucesso", "o nó falhou tecnicamente" e "o nó foi desviado por uma regra de negócio esperada" — os três são semanticamente diferentes e merecem tratamento visual/alertas diferentes.

Modelando errorCode como parte da API do processo

:::tip Boa prática de design Trate os errorCodes lançados por ProcessErrorException como parte do contrato público do seu processo, da mesma forma que trataria códigos de erro HTTP em uma API REST. Documente-os junto à definição do processo (campo description do nó, ou extensionProperties), e evite reutilizar o mesmo código para situações semanticamente diferentes — o Navigator faz o casamento por igualdade exata de string, então qualquer inconsistência de nomenclatura ("documento_invalido" vs. "DOCUMENTO_INVALIDO") silenciosamente faz o erro cair no caminho de incidente técnico em vez de ser tratado pelo fluxo. :::