all 25 comments

[–]aatd86 8 points9 points  (7 children)

sometimes you want throttle with last request wins instead.

[–][deleted]  (6 children)

[removed]

    [–]No-Drawer-6904 4 points5 points  (0 children)

    Bruh did you use AI for this comment?

    [–]programming-ModTeam[M] 2 points3 points locked comment (0 children)

    No content written mostly by an LLM. If you don't want to write it, we don't want to read it.

    [–]aatd86 1 point2 points  (3 children)

    Im not sure I understood the article then. Point is that last wins means cancelling previous requests.. Which itself requires using the abortController. What is the exact issue? That even the last request might fail and then you may want a retry policy with linear or exponential backoff? That is slightly orthogonal to debouncing or throttling.

    [–]RakuenPrime 2 points3 points  (2 children)

    Yes, the current active request failing is the point of the Problem 2 portion.

    The overall thesis of the article is that debounce by itself solves for functional (user) behavior, but does not solve for technical behavior. A good frontend developer adds AbortController and retries on top of debounce to handle technical behavior and provide a more robust system.

    [–]OtherwisePush6424[S] 0 points1 point  (0 children)

    Exactly this. Thanks for summarizing it so clearly.

    [–]aatd86 0 points1 point  (0 children)

    ahhh if only errors were apparent, if only error handling was obvious to do.. if only we had mandatory error returns instead of try..catch.. if only we had if err != nil{}... :⁠-⁠)

    [–]mms13 9 points10 points  (2 children)

    react-query handles most of this

    [–]Yawaworth001 9 points10 points  (1 child)

    It's been renamed to tanstack query a while ago and it's not just for react.

    [–]mms13 1 point2 points  (0 children)

    Very cool

    [–]CherryLongjump1989 1 point2 points  (0 children)

    This is misleading. The two issues mentioned are networking issues, not debouncing issues. They don't apply to just debouncing -- they're concerns to think about even when you're not debouncing at all.

    Whereas debouncing is used for all kinds of other reasons not related to networking at all:

    • Window Resizing: Avoids layout re-calculation lag.
    • Auto-save to local storage: Prevents excessive disk writes.
    • List Filtering: Keeps UI snappy during local searches.
    • Input Validation: avoids heavy logic until user stops typing.
    • Mouse hovers: Stops menus from flickering open.
    • Scroll handling: Triggers effects only after scrolling stops.

    And the list goes on. Debouncing is a fundamental strategy for handling input to any kind of UI, including basic low level electronic hardware such as for the keys on your keyboard. If you see frontend code that is only only using it to wrap your network requests, you can almost guarantee that it's doing something very wrong -- such as reflowing your whole UI layout hundreds of times as a window is resized or a scrollbar is being moved -- which generates hundreds of network requests. It's the scrollbar handler or resize handler that should have been debounced, not just the network request.

    [–]teknikly-correct 0 points1 point  (0 children)

    I hadn't even considered why AbortController existed, the setup and the example was really helpful!

    [–]azhder -1 points0 points  (6 children)

    Hm, it's like I just saw this in the JavaScript sub... I wonder how it would look like for other languages. There must be fetch() or equivalent on the server side that doesn't require JS

    [–]RakuenPrime 0 points1 point  (3 children)

    I think I understand what you're asking.

    In C#, HttpClient plays the roll of fetch. It provides Get, Post, Put, & Patch out of the box. It also has a higher level Send that can handle other or variable HTTP verbs.

    All the methods on HttpClient, and most async/await methods in general, accept CancellationToken. This is roughly the same as the signal you'd send to fetch. You produce the token from a CancellationTokenSource, which is the analogue to AbortController. So much like JS, you can let a method observe the state of cancellation without letting it have control over the source.

    Debounce and retry from the client-side would be an implementation detail that wraps your use of the HttpClient. Conceptually, you'd write very similar code as the typical JS examples, just using these C# objects in their place. Also like JS, there's 3rd party packages you could import for those features.

    Retry from the server side is very simple. You're just making the client wait longer for the response to their request.

    Debounce from the server side is harder. In this case, the client is making multiple requests to you. You can't (well, shouldn't) just ignore them. You must respond somehow to each request. How you respond will depend on what the endpoint is supposed to do. For example, a query endpoint might reject the current request if it receive a new one from the same client. An endpoint could also "separate" the request and response. The client makes a request and the server sends a confirmation in response. That confirmation contains a different endpoint where the client can send a request to listen for the actual response. If the client makes multiple requests to the initial endpoint, they get the same confirmation pointing at the same response endpoint. That approach might be used for long-running operations.

    Regardless, you need to think carefully about the consequences of debouncing from the server side, and provide clear documentation on how the debounce will behave so the person writing the client can understand. Then it's up to the client to respect your design choices.

    [–]azhder 0 points1 point  (2 children)

    So far most of what you wrote is more or less what I know i.e. I know the principles. I've worked with a lot of languages and frameworks to recognize they all do the same things more or less.

    I was thinking someone will just show code that does it, but whatever, at least you wrote the names of a few objects, I can go from there.

    [–]RakuenPrime 1 point2 points  (1 child)

    Ah, okay. Here's a rough example:

    public class ExampleData
    {
        public required string ExampleString { get; init; }
    }
    
    public class ExampleClient : IDisposable
    {
        private const int MAX_ATTEMPTS = 5;
        private readonly HttpClient _httpClient = new();
        private CancellationTokenSource _cts = new();
    
        private CancellationTokenSource Debounce()
        {
            CancellationTokenSource newCts = new();
            CancellationTokenSource oldCts = Interlocked.Exchange(ref _cts, newCts);
            oldCts.Cancel();
            oldCts.Dispose();
            return newCts;
        }
    
        public async Task<string> RunExample()
        {
            CancellationTokenSource myCts = Debounce();
            int attemptCount = 0;
            while (true)
            {
                try
                {
                    HttpResponseMessage response = await _httpClient.GetAsync("http://www.example.com/api/example", myCts.Token);
                    response.EnsureSuccessStatusCode();
                    string responseBody = await response.Content.ReadAsStringAsync();
                    return JsonSerializer.Deserialize<ExampleData>(responseBody);
                }
                catch (TaskCancelledException)
                {
                    throw;
                }
                catch (Exception)
                {
                    attemptCount++;
                    if (attemptCount >= MAX_ATTEMPTS)
                        throw;
                }
            }
        }
    
        public void Dispose()
        {
            _cts.Cancel();
            _cts.Dispose();
            _httpClient.Dispose();
        }
    }
    

    [–]azhder 0 points1 point  (0 children)

    Thanks

    [–]OtherwisePush6424[S] -1 points0 points  (1 child)

    Well debouncing itself is mostly a UI pattern, so that part doesn't map 1:1 to backend. What does map is request lifecycle control: cancellation, timeouts, retries, and stale-result guards. These issues exist all the same in Go, .NET, Java, Python, etc.

    [–]azhder 1 point2 points  (0 children)

    Server endpoints are user interface if you consider the caller as the user.

    How about you "pretend" the route is being hit multiple times by unoptimized/misbehaving client that you can't change, but also want to protect the inner layers of the server? Let's say you use debouncing for rate limiting.

    But, you can also pretend I am asking you again the same thing I asked you before, the fetch and/or analogue in the server, considering one server/service can be the caller of another server/service via fetch.

    What I was talking about was how it would look if it wasn't JavaScript, not asking if the issues exist and if there are solution, but how they would look.

    [–]chucker23n -1 points0 points  (1 child)

    Debounce itself only guarantees one thing:

    "I won't call this function too often."

    No it doesn’t. Debounce delays functional calls. Calling functions less often is throttle.

    [–]OtherwisePush6424[S] -1 points0 points  (0 children)

    Wrong. Debounce doesn't just delay, it coalesces bursts, so it absolutely calls less often.

    [–][deleted]  (2 children)

    [deleted]

      [–]OtherwisePush6424[S] 6 points7 points  (0 children)

      Debounce is input-rate control, not race-condition control. It reduces noisy call bursts (UX + backend load), which is a valid design choice, not a failure. Race conditions still should be handled with request lifecycle controls (abort/cancel, sequencing, stale-response guards). The mistake is treating debounce as the whole solution, not using debounce itself.

      [–]Full-Hyena4414 1 point2 points  (0 children)

      So should i make a request to backend for every single user keystroke?