ppy/osu · critical · InvalidOperationException

A {hitObject} was hit before it became hittable!

Error message

A {hitObject} was hit before it became hittable!

What it means

After a hit is registered, StartTimeOrderedHitPolicy.HandleHit() re-checks via CheckHittable to confirm the object was actually hittable at the hit time (startTime + TimeOffset). If CheckHittable returns anything other than ClickAction.Hit, the game's internal invariant is violated: an object was processed as a hit when the policy says it should not have been. This indicates a logic error in the hit pipeline, not a user mistake.

Source

Thrown at osu.Game.Rulesets.Osu/UI/StartTimeOrderedHitPolicy.cs:67

            // Generally when the user has hit way too early.
            if (result == HitResult.None)
                return ClickAction.Shake;

            return ClickAction.Hit;
        }

        public void HandleHit(DrawableHitObject hitObject)
        {
            if (HitObjectContainer == null)
                throw new InvalidOperationException($"{nameof(HitObjectContainer)} should be set before {nameof(HandleHit)} is called.");

            // Hitobjects which themselves don't block future hitobjects don't cause misses (e.g. slider ticks, spinners).
            if (!hitObjectCanBlockFutureHits(hitObject))
                return;

            if (CheckHittable(hitObject, hitObject.HitObject.StartTime + hitObject.Result.TimeOffset, hitObject.Result.Type) != ClickAction.Hit)
                throw new InvalidOperationException($"A {hitObject} was hit before it became hittable!");

            // Miss all hitobjects prior to the hit one.
            foreach (var obj in enumerateHitObjectsUpTo(hitObject.HitObject.StartTime))
            {
                if (obj.Judged)
                    continue;

                if (hitObjectCanBlockFutureHits(obj))
                    ((DrawableOsuHitObject)obj).MissForcefully();
            }
        }

        /// <summary>
        /// Whether a <see cref="HitObject"/> blocks hits on future <see cref="HitObject"/>s until its start time is reached.
        /// </summary>
        /// <param name="hitObject">The <see cref="HitObject"/> to test.</param>
        private static bool hitObjectCanBlockFutureHits(DrawableHitObject hitObject)
            => hitObject is DrawableHitCircle;

View on GitHub (pinned to d9c73e12ad)

Solutions

  1. Ensure the DrawableHitObject's input handling calls CheckHittable before processing the hit — if it returns non-Hit, do not register the hit.
  2. Verify that no mod or custom DrawableHitObject bypasses the hit policy check before calling HandleHit.
  3. Ensure HitObjectContainer ordering matches the start-time ordering expected by StartTimeOrderedHitPolicy.
  4. If this fires in a custom ruleset, audit the entire hit-registration path from input event to HandleHit.

Example fix

// before — HandleHit called without prior CheckHittable check
if (result.IsHit)
    policy.HandleHit(this);

// after — verify hittable before handling
if (result.IsHit && policy.CheckHittable(this, Time.Current, result.Type) == ClickAction.Hit)
    policy.HandleHit(this);
Defensive patterns

Strategy: validation

Validate before calling

// Verify the object is hittable before registering the hit
var clickAction = policy.CheckHittable(hitObject, hitObject.HitObject.StartTime + result.TimeOffset, result.Type);
if (clickAction != ClickAction.Hit)
    return; // do not process the hit
policy.HandleHit(hitObject);

Try / catch

// Wrap HandleHit to catch the invariant violation during development
try
{
    policy.HandleHit(hitObject);
}
catch (InvalidOperationException ex) when (ex.Message.Contains("hit before it became hittable"))
{
    // Log the full hit pipeline state for debugging
    Logger.Log($"Hit invariant violated for {hitObject}. Check hittability before registering hits.", LoggingTarget.Runtime, LogLevel.Error);
    throw;
}

Prevention

When it happens

Trigger: A DrawableHitObject registers a hit judgement and calls HandleHit, but CheckHittable determines the object was not in a hittable state — e.g., a blocking object precedes it, or the hit time is before the object becomes hittable. This happens when the input pipeline processes a hit without first consulting the policy.

Common situations: Custom ruleset modifications to the hit pipeline that bypass the hit-policy check; mods that alter timing windows or hit ordering in ways inconsistent with the policy; multiplayer race conditions where hit times differ between clients; custom DrawableHitObject subclasses that call OnHit directly.

Related errors


AI-assisted analysis of ppy/osu@d9c73e12ad (2026-08-13). Data as JSON: /api/errors/3bcd034ced6badc7. Report an issue: GitHub.