HangfireIO/Hangfire · error · InvalidOperationException
Can not add a continuation: parent background job '{parentId
Error message
Can not add a continuation: parent background job '{parentId}' does not exist. What it means
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.
Source
Thrown at src/Hangfire.Core/ContinuationsSupportAttribute.cs:135
private void AddContinuation(ElectStateContext context, AwaitingState awaitingState)
{
var connection = context.Connection;
var parentId = awaitingState.ParentId;
// We store continuations as a json array in a job parameter. Since there
// is no way to add a continuation in an atomic way, we are placing a
// distributed lock on parent job to prevent race conditions, when
// multiple threads add continuation to the same parent job.
using (connection.AcquireDistributedJobLock(parentId, AddJobLockTimeout))
{
var jobData = connection.GetJobData(parentId);
if (jobData == null)
{
// When we try to add a continuation for a removed job,
// the system should throw an exception instead of creating
// corrupted state.
throw new InvalidOperationException(
$"Can not add a continuation: parent background job '{parentId}' does not exist.");
}
var continuations = GetContinuations(context, parentId);
// Continuation may be already added. This may happen, when outer transaction
// was failed after adding a continuation last time, since the addition is
// performed outside of an outer transaction.
if (!continuations.Exists(x => x.JobId == context.BackgroundJob.Id))
{
continuations.Add(new Continuation { JobId = context.BackgroundJob.Id, Options = awaitingState.Options });
// Set continuation only after ensuring that parent job still
// exists. Otherwise we could create add non-expiring (garbage)
// parameter for the parent job.
SetContinuations(connection, parentId, continuations);
}
View on GitHub (pinned to c236dd0f93)
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.
Example fix
// before var parentId = GetCachedJobId(); // may be stale BackgroundJob.ContinueJobWith(parentId, () => FollowUp()); // after var parentId = BackgroundJob.Enqueue(() => Parent()); // immediately chain BackgroundJob.ContinueJobWith(parentId, () => FollowUp());
Defensive patterns
Strategy: validation
Validate before calling
using (var conn = JobStorage.Current.GetConnection())
{
var data = conn.GetJobData(parentId);
if (data == null)
{
// parent missing — do not call ContinueJobWith
logger.Warn($"Parent job {parentId} not found; skipping continuation.");
return;
}
}
BackgroundJob.ContinueJobWith(parentId, () => FollowUp()); Try / catch
try
{
BackgroundJob.ContinueJobWith(parentId, () => FollowUp());
}
catch (InvalidOperationException ex) when (ex.Message.Contains("does not exist"))
{
logger.Warn($"Parent job {parentId} was removed before continuation registered.");
// optionally re-enqueue parent or alert
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Was unable to initialize a background job '{ctx.BackgroundJo
- Could not get a value of the job parameter `{name}`. See inn
- Storage transaction class must inherit the JobStorageTransac
- Could not get a value of the job parameter `{name}`. See inn
AI-assisted analysis of HangfireIO/Hangfire@c236dd0f93 (2026-08-13).
Data as JSON: /api/errors/8a7fcfa85b77e35e.
Report an issue: GitHub.