π 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?
π The Investigation Journey
My first assumptions were all wrong:
- 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 maddening part? The loop looked perfectly correct:
<!-- This SHOULD create buttons for hours 6-23 -->
@for (int hour = 6; hour < 24; hour++)
{
<button @onclick="() => OnTimeSlotClick(hour, 0)">
@hour:00 <!-- Shows 6:00, 7:00, 8:00... CORRECT! -->
</button>
}
Buttons displayed: 6:00, 7:00, 8:00... but all called OnTimeSlotClick(24, 0) when clicked!
π‘ The Revelation: Closure Capture
The issue was closure capture - a classic C# gotcha that's especially tricky in Blazor!
// What's ACTUALLY happening:
for (int hour = 6; hour < 24; hour++)
{
// β Captures the VARIABLE 'hour', not its VALUE!
button.Click += () => OnTimeSlotClick(hour, 0);
}
// When loop finishes: hour = 24
// ALL click handlers now reference hour = 24!
In C#, lambdas capture variables, not values. By the time any button is clicked, the loop has completed, and hour = 24 (the loop exits when hour < 24 is false).
π οΈ The Simple Fix That Worked
I changed from for to foreach with Enumerable.Range:
<!-- β
THIS WORKS! -->
@foreach (var hour in Enumerable.Range(6, 18)) <!-- 6 to 23 -->
{
<button @onclick="() => OnTimeSlotClick(hour, 0)">
@hour:00
</button>
}
Why this works: Each iteration of foreach creates a new variable in memory, while for reuses the same variable.
π§ Alternative Solutions
1. Local Copy in Loop (Most Common Fix)
@for (int hour = 6; hour < 24; hour++)
{
var currentHour = hour; // β
Local copy
<button @onclick="() => OnTimeSlotClick(currentHour, 0)">
@currentHour:00
</button>
}
2. Method with 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);
}
}
3. Use @key to Force Re-renders
@for (int hour = 6; hour < 24; hour++)
{
<div @key="hour"> <!-- β
Forces new component instance -->
<button @onclick="() => OnTimeSlotClick(hour, 0)">
@hour:00
</button>
</div>
}
π― Why This Happens in Blazor Specifically
Blazor makes this bug more likely because:
- Event handlers are lambdas that execute later
- Component lifecycle delays execution until after render
- State changes trigger re-renders, changing captured variables
- Async nature means lambdas execute long after loop completes
π The Universal Lesson
This isn't just a Blazor problem - it's a closure capture issue that appears in:
-
JavaScript: Same issue with
varin loops - C#: Any lambda in a loop
- Java: Anonymous classes capturing loop variables
- Python: Similar issues with default arguments
The rule: When creating lambdas/events in loops, ensure you're capturing values, not variables.
π€ Debugging Tips for Similar Issues
- Add immediate logging in the lambda
- Test with hardcoded values to isolate the issue
- Check if it's a closure problem by creating local copies
- Remember: Loops with lambdas are always suspicious!
π Final Thoughts
This bug taught me that sometimes the simplest-looking code can have the most surprising behavior. What looks like a primitive int in a loop actually becomes a shared reference across all event handlers.
Have you encountered similar closure bugs? What's your favorite "it can't be that" debugging moment?
Discussion:
- Have you been bitten by closure capture bugs?
- What other Blazor gotchas have you encountered?
- Share your favorite debugging stories!
Top comments (0)