logoalt Hacker News

b0a04glyesterday at 6:44 PM2 repliesview on HN

in async code ,errors belong to the task ,not the caller.

in sync code ,the caller owns the stack ,so it makes sense they own the error. but async splits that. now each async function runs like a background job. that job should handle its own failure =retry ,fallback ,log because the caller usually cant do much anyway.

write async blocks like isolated tasks. contain errors inside unless the caller has a real decision to make. global error handler picks up the rest


Replies

quietbritishjimyesterday at 8:22 PM

Structured concurrency [1] solves the issue of task (and exception) ownership. In languages / libraries that support it, when spawning a task you must specify some enclosing block that owns it. That block, called a nursery or task group, can be a long way outside the point where the task is spawned because the nursery is an object in its own right, so it can be passed into a function which can then call its start() method. All errors are handled at the nursery level.

They were introduced in the Trio library [2] for Python, but they're now also supported by Python's built in asyncio module [3]. I believe the idea has spread to other languages too.

[1] https://vorpus.org/blog/notes-on-structured-concurrency-or-g...

[2] https://trio.readthedocs.io/en/stable/

[3] https://docs.python.org/3/library/asyncio-task.html#task-gro...

EGregyesterday at 6:53 PM

well, that's partially true

the caller is itself a task / actor

the thing is that the caller might want to rollback what they're doing based on whether the subtask was rolled back... and so on, backtracking as far as needed

ideally all the side effects should be queued up and executed at the end only, after your caller has successfully heard back from all the subtasks

for example... don't commit DB transactions, send out emails or post transactions onto a blockchain until you know everything went through. Exceptions mean rollback, a lot of the time.

on the other hand, "after" hooks are supposed to happen after a task completes fully, and their failure shouldn't make the task rollback anything. For really frequent events, you might want to debounce, as happens for example with browser "scroll" event listeners, which can't preventDefault anymore unless you set them with {passive: false}!

PS: To keep things simple, consider using single-threaded applications. I especially like PHP, because it's not only single-threaded but it actually is shared-nothing. As soon as your request handling ends, the memory is released. Unlike Node.js you don't worry about leaking memory or secrets between requests. But whether you use PHP or Node.js you are essentially running on a single thread, and that means you can write code that is basically sequentially doing tasks one after the other. If you need to fan out and do a few things at a time, you can do it with Node.js's Promise.all(), while with PHP you kind of queue up a bunch of closures and then explicitly batch-execute with e.g. curl_multi_ methods. Either way ... you'll need to explicitly write your commit logic in the end, e.g. on PHP's "shutdown handler", and your database can help you isolate your transactions with COMMIT or ROLLBACK.

If you organize your entire code base around dispatching events instead of calling functions, as I did, then you can easily refactor it to do things like microservices at scale by using signed HTTPS requests as a transport (so you can isolate secrets, credentials, etc.) from the web server: https://github.com/Qbix/Platform/commit/a4885f1b94cab5d83aeb...

show 2 replies