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.
:::