DEV Community

Taher Maimoon
Taher Maimoon

Posted on

The Blazor Closure Bug That Made All My Time Slots 24:00 πŸ•› πŸ•›

πŸ› 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
Enter fullscreen mode Exit fullscreen mode

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:

  1. Timezone issue? - Checked DateTime.Kind, UTC conversions
  2. DateTime parsing bug? - Added extensive validation
  3. 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!
}
Enter fullscreen mode Exit fullscreen mode

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>
}
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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>
}
Enter fullscreen mode Exit fullscreen mode

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>
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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>
}
Enter fullscreen mode Exit fullscreen mode

🎯 Why This Happens in Blazor Specifically

Blazor makes this bug more likely because:

  1. Event handlers are lambdas that execute later
  2. Component lifecycle delays execution until after render
  3. State changes trigger re-renders, changing captured variables
  4. 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 var in 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

  1. Add immediate logging in the lambda
  2. Test with hardcoded values to isolate the issue
  3. Check if it's a closure problem by creating local copies
  4. 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)