HangfireIO/Hangfire · error · ArgumentException
All the threads should be non-null and in the ThreadState.Un
Error message
All the threads should be non-null and in the ThreadState.Unstarted state.
What it means
Thrown by the BackgroundDispatcher constructor (internal) when the threadFactory callback returns at least one thread that is null or whose ThreadState does not include Unstarted. The dispatcher owns thread lifecycle: it calls thread.Start() itself (line 71), so handing it an already-started or null thread violates the ownership contract and would double-start or NRE.
Source
Thrown at src/Hangfire.Core/Processing/BackgroundDispatcher.cs:64
_execution = execution ?? throw new ArgumentNullException(nameof(execution));
_action = action ?? throw new ArgumentNullException(nameof(action));
_state = state;
#if !NETSTANDARD1_3
AppDomainUnloadMonitor.EnsureInitialized();
#endif
var threads = threadFactory(DispatchLoop)?.ToArray();
if (threads == null || threads.Length == 0)
{
throw new ArgumentException("At least one unstarted thread should be created.", nameof(threadFactory));
}
if (threads.Any(static thread => thread == null || (thread.ThreadState & ThreadState.Unstarted) == 0))
{
throw new ArgumentException("All the threads should be non-null and in the ThreadState.Unstarted state.", nameof(threadFactory));
}
_stopped = new CountdownEvent(threads.Length);
foreach (var thread in threads)
{
thread.Start();
}
}
public bool Wait(TimeSpan timeout)
{
return _stopped.WaitHandle.WaitOne(timeout);
}
public async Task WaitAsync(TimeSpan timeout, CancellationToken cancellationToken)
{
await _stopped.WaitHandle.WaitOneAsync(timeout, cancellationToken).ConfigureAwait(false);View on GitHub (pinned to c236dd0f93)
Solutions
- Make the thread factory return only freshly-created Thread objects created with `new Thread(start)` and never call Start() on them.
- Filter out nulls before returning: `threads.Where(t => t != null)` and ensure the result is non-empty.
- If reusing Hangfire's pattern, copy DefaultThreadFactory from BackgroundTaskScheduler which constructs unstarted background threads.
Example fix
// before
Func<ThreadStart, IEnumerable<Thread>> factory = start =>
new[] { new Thread(start) { IsBackground = true } };
var t = new Thread(factory.First()); t.Start();
return new[] { t }; // already started -> throws
// after
Func<ThreadStart, IEnumerable<Thread>> factory = start =>
new[] { new Thread(start) { IsBackground = true, Name = "Worker" } }; // unstarted Defensive patterns
Strategy: validation
Validate before calling
// Validate a thread factory's output is all non-null and unstarted before passing it on.
bool IsValidFactory(Func<ThreadStart, IEnumerable<Thread>> factory, ThreadStart start)
{
var threads = factory(start)?.ToArray();
return threads != null && threads.Length > 0 &&
threads.All(t => t != null && (t.ThreadState & System.Threading.ThreadState.Unstarted) != 0);
} Type guard
static bool IsUnstartedThread(Thread t) =>
t != null && (t.ThreadState & System.Threading.ThreadState.Unstarted) != 0; Try / catch
try { var d = new BackgroundDispatcher(exec, action, state, factory); }
catch (ArgumentException ex) when (ex.ParamName == nameof(factory))
{ /* log and rebuild factory to return fresh unstarted threads */ } Prevention
- Never call Thread.Start() inside a thread factory — let the dispatcher own lifecycle.
- Always create threads with `new Thread(start)` and set IsBackground/Name only.
- Filter nulls out of the returned collection.
When it happens
Trigger: Constructing BackgroundDispatcher with a Func<ThreadStart, IEnumerable<Thread>> whose result contains a null element or a thread already started elsewhere (ThreadState has the Unstarted bit cleared). Reached indirectly through Hangfire server wiring that supplies a custom thread factory.
Common situations: A custom server/hosting extension builds dedicated dispatcher threads and accidentally calls thread.Start() before returning them; a factory reuses/caches a Thread instance that was previously started; a factory returns null in place of a thread on some code path.
Related errors
AI-assisted analysis of HangfireIO/Hangfire@c236dd0f93 (2026-08-13).
Data as JSON: /api/errors/52ddfb0279578611.
Report an issue: GitHub.