{"record":{"id":"c4cc393e153bdc2d","repo":"temporalio/temporal","slug":"unknown-workflow-update-abort-reason-s-or-update","errorCode":null,"errorMessage":"unknown workflow update abort reason %s or update state %s","messagePattern":"unknown workflow update abort reason (.+?) or update state (.+?)","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"service/history/workflow/update/abort_reason.go","lineNumber":111,"sourceCode":"\t// Updates which *have* been seen by the Workflow are aborted with non-retryable error.\n\t// Failed WFT will be retried but Update must not. Otherwise, internal retries will exhaust and Unavailable error will be returned to the client.\n\treasonState{r: AbortReasonWorkflowTaskFailed, st: stateSent}: {f: nil, err: workflowTaskFailErr},\n\t// Updates which passed Accepted state are not retried when the registry is cleared, so there is no need to abort them.\n\treasonState{r: AbortReasonWorkflowTaskFailed, st: stateProvisionallyAccepted}:               {f: nil, err: nil},\n\treasonState{r: AbortReasonWorkflowTaskFailed, st: stateAccepted}:                            {f: nil, err: nil},\n\treasonState{r: AbortReasonWorkflowTaskFailed, st: stateProvisionallyCompleted}:              {f: nil, err: nil},\n\treasonState{r: AbortReasonWorkflowTaskFailed, st: stateProvisionallyCompletedAfterAccepted}: {f: nil, err: nil},\n\treasonState{r: AbortReasonWorkflowTaskFailed, st: stateCompleted}:                           {f: nil, err: nil},\n\treasonState{r: AbortReasonWorkflowTaskFailed, st: stateProvisionallyAborted}:                {f: nil, err: nil},\n\treasonState{r: AbortReasonWorkflowTaskFailed, st: stateAborted}:                             {f: nil, err: nil},\n}\n\n// FailureError returns failure or error which will be set on Update futures while aborting Update.\n// Only one of the return values will be non-nil.\nfunc (r AbortReason) FailureError(st state) (*failurepb.Failure, error) {\n\tfe, ok := reasonStateMatrix[reasonState{r: r, st: st}]\n\tif !ok {\n\t\tpanic(fmt.Sprintf(\"unknown workflow update abort reason %s or update state %s\", r, st))\n\t}\n\treturn fe.f, fe.err\n}\n\nfunc (r AbortReason) String() string {\n\tswitch r {\n\tcase AbortReasonRegistryCleared:\n\t\treturn \"RegistryCleared\"\n\tcase AbortReasonWorkflowCompleted:\n\t\treturn \"WorkflowCompleted\"\n\tcase AbortReasonWorkflowContinuing:\n\t\treturn \"WorkflowContinuing\"\n\tcase AbortReasonWorkflowTaskFailed:\n\t\treturn \"WorkflowTaskFailed\"\n\tcase lastAbortReason:\n\t\treturn fmt.Sprintf(\"invalid reason %d\", r)\n\t}\n\treturn fmt.Sprintf(\"unrecognized reason %d\", r)","sourceCodeStart":93,"sourceCodeEnd":129,"githubUrl":"https://github.com/temporalio/temporal/blob/bde624efd13fbd3843654058db6d9c716166318b/service/history/workflow/update/abort_reason.go#L93-L129","documentation":"AbortReason.FailureError maps an (AbortReason, state) pair to the Failure/error placed on update futures during abort. The mapping lives in reasonStateMatrix; when the combination has no entry, the code assumes an internal invariant was violated and panics with this message. This is a programmer/protocol error inside the update subsystem, not user input validation.","triggerScenarios":"A new AbortReason value or update state was added without a corresponding entry in reasonStateMatrix, or abort() was called with a state that is not legal for that reason (e.g. aborting an already-completed update with a reason defined only for running states).","commonSituations":"Developers extending the workflow update state machine (adding an abort reason or state) in the Temporal server; backporting partial changes between versions; tests constructing AbortReason/state pairs directly and passing an illegal combination.","solutions":["Add the missing (AbortReason, state) entry to reasonStateMatrix in service/history/workflow/update/abort_reason.go for the pair printed in the panic message","Check the abort call path to ensure the update state passed to FailureError is legal for that reason (guard with AbortReason.AssertRunnable or similar state checks before aborting)","If the values are computed (e.g. deserialized from persistence), validate them against known enums before calling FailureError"],"exampleFix":"// before (reason added without matrix entry)\nconst (\n\tAbortReasonFoo AbortReason = iota + abortReasonSentinel\n)\n\n// after\nconst (\n\tAbortReasonFoo AbortReason = iota + abortReasonSentinel\n)\n\nfunc init() {\n\treasonStateMatrix[reasonState{r: AbortReasonFoo, st: StateUnspecified}] = reasonStateInfo{}\n\treasonStateMatrix[reasonState{r: AbortReasonFoo, st: StateAdmitted}] = reasonStateInfo{}\n}","handlingStrategy":"validation","validationCode":"// Validate (reason, state) is legal before aborting\nif _, ok := update.ReasonStateMatrixLookup(reason, st); !ok {\n\treturn fmt.Errorf(\"illegal abort reason %v for update state %v\", reason, st)\n}","typeGuard":"func knownAbortReasonState(r update.AbortReason, st update.State) bool {\n\t_, ok := update.ReasonStateMatrixLookup(r, st)\n\treturn ok\n}","tryCatchPattern":"// Panics are not recoverable by convention here; prefer validating above.\n// If wrapping a test harness:\nfunc safeFailureError(r update.AbortReason, st update.State) (fe *failurepb.Failure, err error) {\n\tdefer func() {\n\t\tif rec := recover(); rec != nil {\n\t\t\terr = fmt.Errorf(\"abort mapping panic: %v\", rec)\n\t\t}\n\t}()\n\treturn r.FailureError(st)\n}","preventionTips":["Every time you add an AbortReason or update State, add entries for all legal pairs in reasonStateMatrix and run the matrix completeness test","Add a unit test that iterates the full AbortReason × State cross-product and asserts which pairs must be present","Validate deserialized reason/state enums from persistence before use"],"tags":["go","panic","workflow-updates","invariant-violation"],"backgroundTag":"unhandled-enum-state","analyzedSha":"bde624efd13fbd3843654058db6d9c716166318b","analyzedAt":"2026-09-01T07:18:39.080Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T15:18:49.778Z"}