The Blazor Closure Bug That Made All My Time Slots 24:00 🕛 🕛

Published: (January 3, 2026 at 02:41 PM EST)
3 min read
Source: Dev.to

Source: Dev.to

The Mysterious Bug

I was building a conference room booking system in Blazor when I encountered a bizarre bug. My timeline component showed time slots from 6 AM to 11 PM, but whenever I clicked any time slot, it would always show 24:00 in the console.

// Console output – ALWAYS showed 24!
Time slot selected: 24:0
Start: 06-01-2026 00:00:00, End: 06-01-2026 01:00:00

The timeline looked correct visually, but every single button was broken. How could all buttons from 6 AM to 11 PM send “24” as the hour?

First assumptions

  • Timezone issue? – Checked DateTime.Kind, UTC conversions
  • DateTime parsing bug? – Added extensive validation
  • Browser datetime-local bug? – Tested across browsers

After 3 hours of debugging, I added more logging:

private async Task OnTimeSlotClick(int hour, int minute)
{
    Console.WriteLine($"[DEBUG] Clicked hour: {hour}");
    // Always showed 24, regardless of which button!
}

The loop that generated the buttons looked perfectly correct:

@for (int hour = 6; hour < 24; hour++)
{
    <button @onclick="() => OnTimeSlotClick(hour, 0)">
        @hour:00
    </button>
}

Buttons displayed 6:00, 7:00, 8:00… but all called OnTimeSlotClick(24, 0) when clicked!

The closure capture gotcha

In C#, lambdas capture variables, not their values. By the time any button is clicked, the loop has completed and hour == 24 (the loop exits when hour reaches 24).

// When loop finishes: hour = 24
// ALL click handlers now reference hour = 24!

Fixes

1. Use foreach with Enumerable.Range

@foreach (var hour in Enumerable.Range(6, 18))
{
    <button @onclick="() => OnTimeSlotClick(hour, 0)">
        @hour:00
    </button>
}

Each iteration of foreach creates a new variable, avoiding the shared capture.

2. Local copy inside the for loop (most common fix)

@for (int hour = 6; hour < 24; hour++)
{
    var currentHour = hour;
    <button @onclick="() => OnTimeSlotClick(currentHour, 0)">
        @currentHour:00
    </button>
}

3. Method with a parameter

@for (int hour = 6; hour < 24; hour++)
{
    <button @onclick="() => HandleHourClick(hour)">
        @hour:00
    </button>
}

@code {
    private void HandleHourClick(int hour) // ✅ New parameter each call
    {
        OnTimeSlotClick(hour, 0);
    }
}

4. Use @key to force re‑renders

@for (int hour = 6; hour < 24; hour++)
{
    <button @key="hour" @onclick="() => OnTimeSlotClick(hour, 0)">
        @hour:00
    </button>
}

Why Blazor makes this bug more likely

  • Event handlers are lambdas that execute later.
  • Component lifecycle delays execution until after render.
  • State changes trigger re‑renders, potentially changing captured variables.
  • The async nature means lambdas often run long after the loop finishes.

It’s not just a Blazor problem

  • JavaScript – same issue with var in loops.
  • C# – any lambda inside a loop.
  • Java – anonymous classes capturing loop variables.
  • Python – default arguments evaluated at definition time.

The rule of thumb

When creating lambdas or event handlers inside loops, capture the value, not the variable.

  • Add immediate logging inside the lambda.
  • Test with hard‑coded values to isolate the issue.
  • Create a local copy (var current = loopVar) before the lambda.
  • Treat loops with lambdas as “always suspicious”.

Closing thoughts

This bug taught me that the simplest‑looking code can have the most surprising behavior. An int in a loop can become a shared reference across all event handlers if you’re not careful.

Discussion

  • Have you been bitten by closure capture bugs?
  • What other Blazor gotchas have you encountered?
  • Share your favorite debugging stories!
Back to Blog

Related posts

Read more »