Why can't spawned thread functions return errors?

When using Thread.spawn, the function to use can only return void. This is inconvenient because you either have to wrap your real function in a function that catches and handles the errors and itself returns void, or you must handle each and every error when it happens in your primary function. I wounder why the return type couldn’t have been something like anyerror!void?


Some random thoughts:

See: Why request functions must not return errors

Suppose that thread functions can return an error.

Which thread is going to handle the error?

  1. The same thread
    There is no added value in returning an error.
  2. The main thread of the process
    Error handling in this case is no different from the option 1.
  3. The initiating thread that calls spawn
    It is not always possible; the initiating thread might finish after calling spawn.
    This case is meaningful if join is called to wait for the spawned thread termination.

Erlang style concurrency is perfect not only because of the messaging between processes, but because of the process crashing handling that happens between processes.

I’m not familiar with Erlang but I find Rust’s handling of this quite powerful. The join method will return a Result<T, E> letting you obtain any error the thread might have returned and with the added bonus of also allowing you to obtain any value the thread function might have returned on success. So you pretrty much handle a spawned thread like calling a normal function.

Inspecting the Rust docs on this result type for threads, I see there’s a Box<dyn... there, so I take it there’s a behind-the-scenes allocation taking place to store the error. This wouldn’t fly with Zig, unless the spawn function would require an allocator parameter for this. I see it already takes a SpawnConfig that lets you specify an allocator, so maybe the infrastructure is almost there already.

1 Like

How could you return something from one thread and have that result appear in another thread?
Small values are returned in a register, but a register is not visible to other threads, and even if it were, you would need to get it out of there before it got clobbered, after all, that processor is going to need its registers to continue working (even if that work is for the operating system or for another program).
Bigger values are returned on the stack, but you can’t push stuff onto the original stack, because it’s being used by the original thread, and this thread’s stack is going to be freed once the thread ends.
A return instruction also jumps to an address to continue execution, but if the thread is ending, there’s no address to jump to. In reality, thread function return types are noreturn as far as the OS is concerned (you need to exit them through some other mean that is not a return statement). You should never return from the end of the stack, as you’ll simply jump onto garbage. Zig might be using void instead of noreturn because it’s probably wrapping the user provided function, so that it can do extra things once the user provided code returns, like some cleanup.
This shows why the refurn mechanism doesn’t apply to threads. With that said, it’s pretty easy to exchange stuff among threads. In the calling thread, make some space for the return value (it could be on stack, if it’s not going out of scope before the threand ends), provide a pointer to that value to the spawning thread, and simply have it write the output there. You can even create a small utility that wraps stuff for you and create the illusion of values being returned.


Wouldn’t it be nice if said utility, all thread- and concurrent-safe implemented, was part of the standard offering? I think it would … A “channel” receiving your final good-bye automatically wrapped threadfn return value “message”

1 Like

You have presented all the technical reasons why this shouldn’t be possible or at least not possible in a way that’s not exponentially more complex than what Zig has now. But as I said, the folks over at Rust have already solved this problem. How they did it, I have no idea, but benchmarks of multi-threaded Rust code definitely prove that whatever they’re doing doesn’t impact performance much.

To be pedantic, the error that a thread returned would be Result<Result<T, E>>. That is, the actual error value from the user supplied function would be the inner result. The outre result is for catching panics, not for normal error errors.

1 Like