The Blazor Closure Bug That Made All My Time Slots 24:00 🕛 🕛
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-localbug? – 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
varin 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!