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

  1. Make the thread factory return only freshly-created Thread objects created with `new Thread(start)` and never call Start() on them.
  2. Filter out nulls before returning: `threads.Where(t => t != null)` and ensure the result is non-empty.
  3. 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

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.