10 votes

The await event horizon in Javascript

4 comments

  1. [2]
    Johz
    Link
    Structured concurrency is on my list of "probably really useful ideas that I should explore when I get the chance". I think a lot of discussion about async/await (particularly in languages like...

    Structured concurrency is on my list of "probably really useful ideas that I should explore when I get the chance". I think a lot of discussion about async/await (particularly in languages like Rust, which are currently exploring what that should look like), would benefit from a deeper understanding of structured concurrency.

    I think this article kind of does structured concurrency a disfavour by concentrating on the divergence issue — i.e. what happens if I call a function and it just runs forever. This can be important, but it's usually important in the context of other things. After all, most servers essentially act like infinite loops once they're started, and that's okay in that context.

    I think the better example is when you create a new promise without necessarily awaiting it. If you await the promise, then the function you're in becomes bound to the promise you're awaiting — the function can only progress when that promise has completed. But if you don't await the promise, then the promise is unbound, and the function execution and the promise execution no longer have anything to do with each other. The function can return with the promise still hanging on somewhere. And if the promise never resolves as described in the article, then we essentially end up with a leaked resource floating around.

    The problem in Javascript (and many other languages) is that it's very easy to create these unbound promises, and you need to use discipline to keep track of what resources are being controlled. Structured concurrency, at least as far as I understand it, is a tool that prevents us from creating unbound executions, and ensures that resources are always owned somewhere in the call stack.

    6 votes
    1. skybrian
      Link Parent
      Yes, one of the more annoying bugs I’ve run into is forgetting an await. There shouldn’t be any default behavior for calling an async function - either you await it or you spawn it, and there...

      Yes, one of the more annoying bugs I’ve run into is forgetting an await. There shouldn’t be any default behavior for calling an async function - either you await it or you spawn it, and there should be keywords for both. (In Go, this is the “go” keyword for a spawn, but there’s no “await” keyword because it has a preemptive scheduler.)

      Divergence does happen, but even infinite loops shouldn’t really run forever. In a decent UI, any long-running task has a cancel button. There needs to be some way of shutting it down anything, even a long-running server. Traditionally, this is at the process level with an interrupt, such as pressing Control-C. In a browser, there are the stop and reload buttons.

      I find the argument that cancellation should work automatically at the function call level to be pretty convincing, in theory. I’m willing to live with workarounds, though, rather than adopt an unnatural coding style. Maybe a future programming language will get it right?

      2 votes
  2. [2]
    skybrian
    (edited )
    Link
    Here’s a blog post arguing that it should be possible to abort an async operation at any time, freeing any resources. Structured concurrency libraries can help make this happen. In JavaScript,...

    Here’s a blog post arguing that it should be possible to abort an async operation at any time, freeing any resources.

    Structured concurrency libraries can help make this happen. In JavaScript, this is apparently built using generators, which have a return method for canceling an operation. Unfortunately, this means you can’t use async/await, which seems like a high price to pay. The Effection library has a page explaining what to use instead.

    In Go, the context package has similar functionality for dealing with cancellation and timeouts. In an RPC system built using microservices, a top-level deadline should be passed along with each remote call, which in turn gets added to a child operation’s context. I believe this is best-effort, though, rather than a guarantee?

    I’m not sure how necessary it is in a web page, either. You can always reload the page.

    3 votes
    1. slambast
      Link Parent
      Yes - there's nothing stopping you from constructing a function like this: func TotallyConformantFunction(ctx context.Context) { newCtx := context.Background() doTaskThatShouldBeCancelable(newCtx)...

      a top-level deadline should be passed along with each remote call, which in turn gets added to a child operation’s context. I believe this is best-effort, though, rather than a guarantee?

      Yes - there's nothing stopping you from constructing a function like this:

      func TotallyConformantFunction(ctx context.Context) {
      	newCtx := context.Background()
      	doTaskThatShouldBeCancelable(newCtx)
      }
      

      ...and lo, the original context's CancelFunc does nothing. Not only can it be escaped on purpose, it requires a bit of discipline to remember to handle cancellations. You might, for some god-forsaken reason, want to have a function like this:

      func WaitFor3Seconds(_ context.Context) error {
      	time.Sleep(3 * time.Second)
      	return nil
      }
      

      But this will ignore context cancellation. What you have to do instead is:

      func WaitFor3Seconds(ctx context.Context) error {
      	timer := time.NewTimer(3 * time.Second)
      	select {
      	case <-timer.C:
      		return nil
      	case <-ctx.Done():
      		timer.Stop()
      		return ctx.Err()
      	}
      }
      
      4 votes