dotnet/yarp · error · InvalidOperationException

An uninitialized, or 'default', ValueStopwatch cannot be use

Error message

An uninitialized, or 'default', ValueStopwatch cannot be used to get elapsed time.

What it means

ValueStopwatch is a struct that stores only a start timestamp; reading Elapsed on a default(ValueStopwatch) (start timestamp 0) is meaningless, so the getter throws InvalidOperationException. The zero check is the library's way of detecting an uninitialized instance, since StartNew is the only constructor and a real timestamp can never be 0. YARP uses this internally for cheap timing in hot paths.

Source

Thrown at src/ReverseProxy/Utilities/ValueStopwatch.cs:37

    private readonly long _startTimestamp;

    private ValueStopwatch(long startTimestamp)
    {
        _startTimestamp = startTimestamp;
    }

    /// <summary>
    /// Gets the time elapsed since the stopwatch was created with <see cref="StartNew"/>.
    /// </summary>
    public TimeSpan Elapsed
    {
        get
        {
            // Start timestamp can't be zero in an initialized ValueStopwatch. It would have to be literally the first thing executed when the machine boots to be 0.
            // So it being 0 is a clear indication of default(ValueStopwatch)
            if (_startTimestamp == 0)
            {
                throw new InvalidOperationException("An uninitialized, or 'default', ValueStopwatch cannot be used to get elapsed time.");
            }

            var end = Stopwatch.GetTimestamp();
            var timestampDelta = end - _startTimestamp;
            var ticks = (long)(_timestampToTicks * timestampDelta);
            return new TimeSpan(ticks);
        }
    }

    /// <summary>
    /// Creates a new <see cref="ValueStopwatch"/> that is ready to be used.
    /// </summary>
    public static ValueStopwatch StartNew() => new ValueStopwatch(Stopwatch.GetTimestamp());
}

View on GitHub (pinned to bd11867bee)

Solutions

  1. Initialize the stopwatch eagerly with ValueStopwatch.StartNew() at declaration or in the constructor.
  2. Track a separate bool _started flag and guard the Elapsed read behind it.
  3. Use ValueStopwatch? (nullable) and check HasValue before reading Elapsed.
  4. If timing is optional, prefer nullable ValueStopwatch? over a sentinel default instance.

Example fix

// before
private ValueStopwatch _stopwatch;
public TimeSpan Elapsed => _stopwatch.Elapsed; // throws if never started

// after
private ValueStopwatch? _stopwatch;
public TimeSpan Elapsed => _stopwatch?.Elapsed ?? TimeSpan.Zero;
Defensive patterns

Strategy: validation

Validate before calling

ValueStopwatch? sw = null;
// only start when needed:
sw = ValueStopwatch.StartNew();
// read defensively:
var elapsed = sw?.Elapsed ?? TimeSpan.Zero;

Type guard

static bool IsRunning(ValueStopwatch sw) => sw != default;
// usage:
if (IsRunning(_stopwatch)) { var e = _stopwatch.Elapsed; }

Try / catch

try { var e = _stopwatch.Elapsed; }
catch (InvalidOperationException) { /* stopwatch not started; skip timing */ }

Prevention

When it happens

Trigger: Declaring a ValueStopwatch field, property, or local without assigning ValueStopwatch.StartNew(), then reading .Elapsed. For example a struct holding an optional timer where the timer is left default in some code path, or a field initialized only inside an if-branch that is later read unconditionally.

Common situations: A conditional StartNew() inside an if-block but an unconditional Elapsed read; refactoring a class stopwatch into a struct field and forgetting the initializer; default-constructing a parent struct that embeds a ValueStopwatch; sharing a stopwatch across async paths where one path never starts it.

Related errors


AI-assisted analysis of dotnet/yarp@bd11867bee (2026-08-13). Data as JSON: /api/errors/be9932c4ca06e45f. Report an issue: GitHub.