{"record":{"id":"8a7fcfa85b77e35e","repo":"HangfireIO/Hangfire","slug":"can-not-add-a-continuation-parent-background-job","errorCode":null,"errorMessage":"Can not add a continuation: parent background job '{parentId}' does not exist.","messagePattern":"Can not add a continuation: parent background job '(.+?)' does not exist\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"src/Hangfire.Core/ContinuationsSupportAttribute.cs","lineNumber":135,"sourceCode":"\n        private void AddContinuation(ElectStateContext context, AwaitingState awaitingState)\n        {\n            var connection = context.Connection;\n            var parentId = awaitingState.ParentId;\n\n            // We store continuations as a json array in a job parameter. Since there \n            // is no way to add a continuation in an atomic way, we are placing a \n            // distributed lock on parent job to prevent race conditions, when\n            // multiple threads add continuation to the same parent job.\n            using (connection.AcquireDistributedJobLock(parentId, AddJobLockTimeout))\n            {\n                var jobData = connection.GetJobData(parentId);\n                if (jobData == null)\n                {\n                    // When we try to add a continuation for a removed job,\n                    // the system should throw an exception instead of creating\n                    // corrupted state.\n                    throw new InvalidOperationException(\n                        $\"Can not add a continuation: parent background job '{parentId}' does not exist.\");\n                }\n\n                var continuations = GetContinuations(context, parentId);\n\n                // Continuation may be already added. This may happen, when outer transaction\n                // was failed after adding a continuation last time, since the addition is\n                // performed outside of an outer transaction.\n                if (!continuations.Exists(x => x.JobId == context.BackgroundJob.Id))\n                {\n                    continuations.Add(new Continuation { JobId = context.BackgroundJob.Id, Options = awaitingState.Options });\n\n                    // Set continuation only after ensuring that parent job still\n                    // exists. Otherwise we could create add non-expiring (garbage)\n                    // parameter for the parent job.\n                    SetContinuations(connection, parentId, continuations);\n                }\n","sourceCodeStart":117,"sourceCodeEnd":153,"githubUrl":"https://github.com/HangfireIO/Hangfire/blob/c236dd0f930f831ec151e436e138ddc429a02a72/src/Hangfire.Core/ContinuationsSupportAttribute.cs#L117-L153","documentation":"ContinuationsSupportAttribute (ContinuationsSupportAttribute.cs:135) stores continuation chains as a parameter on the parent job. Before appending a continuation it loads the parent's JobData; if the parent does not exist (was deleted, expired, or never created), it throws to prevent creating a dangling/orphaned continuation that could never fire.","triggerScenarios":"Calling BackgroundJob.ContinueJobWith(parentId, ...) where parentId refers to a job that was already deleted, removed by retention, or was never enqueued. Also occurs when the parent job ID is malformed or belongs to a different storage.","commonSituations":"Parent job completed and was cleaned up by a retention job before the continuation was registered; passing a hardcoded or stale job ID; multi-tenant setups where the ID belongs to another tenant's storage; race with manual job deletion.","solutions":["Verify the parent job exists via monitoringApi.JobDetails(parentId) or connection.GetJobData(parentId) before calling ContinueJobWith.","Register the continuation immediately after enqueueing the parent (capture the returned ID) rather than using a stored/external ID.","Increase or disable job expiration if continuations are added asynchronously long after the parent ran.","Wrap the call in a try/catch if a missing parent is a recoverable condition and handle gracefully."],"exampleFix":"// before\nvar parentId = GetCachedJobId(); // may be stale\nBackgroundJob.ContinueJobWith(parentId, () => FollowUp());\n\n// after\nvar parentId = BackgroundJob.Enqueue(() => Parent());\n// immediately chain\nBackgroundJob.ContinueJobWith(parentId, () => FollowUp());","handlingStrategy":"validation","validationCode":"using (var conn = JobStorage.Current.GetConnection())\n{\n    var data = conn.GetJobData(parentId);\n    if (data == null)\n    {\n        // parent missing — do not call ContinueJobWith\n        logger.Warn($\"Parent job {parentId} not found; skipping continuation.\");\n        return;\n    }\n}\nBackgroundJob.ContinueJobWith(parentId, () => FollowUp());","typeGuard":null,"tryCatchPattern":"try\n{\n    BackgroundJob.ContinueJobWith(parentId, () => FollowUp());\n}\ncatch (InvalidOperationException ex) when (ex.Message.Contains(\"does not exist\"))\n{\n    logger.Warn($\"Parent job {parentId} was removed before continuation registered.\");\n    // optionally re-enqueue parent or alert\n}","preventionTips":["Chain continuations immediately after enqueueing the parent to capture the returned ID.","Check parent existence via GetJobData before ContinueJobWith if the parent ID is stale.","Be aware that job retention/expiration can remove parents before late continuations."],"tags":["continuations","missing-job","state","race-condition"],"backgroundTag":null,"analyzedSha":"c236dd0f930f831ec151e436e138ddc429a02a72","analyzedAt":"2026-08-13T20:27:11.027Z","schemaVersion":2},"datasetVersion":"2026-08-14T00:17:13.853Z"}