Yeah, that's quite nasty. But spawn*() has all the same problems and a bunch of extra ones, prominent among them the fact that it's exactly the same as fork()/exec() with the code between the fork() and exec() hardwired. So you end up with dozens of spawn*() calls and no benefit over fork()/exec() at all (except on tiny non-MMU systems, which can theoretically implement spawn*() but not fork()/exec() --- but usually implement both, because most code uses fork()/exec() and not spawn*().)
I suspect that a combination of waitpid() (to catch signals and read-from-pipe to catch errno might work: if you played games with self-signalling you could possibly encode errnos as rare signals and drop the pipe, at the cost of losing the ability to detect those rare signals.
A bit of extra effort (sending something down the pipe right before exec() as well as right after a failed one, and opening the pipe end O_CLOEXEC) enables you to distinguish between a signal hitting before exec() and a signal hitting after a successful exec().
But, yes, this is all pointlessly complex. If C had proper Lisp-style macros we could wrap this up in a library without the result becoming as inflexible as spawn(). (Something involving function pointers could get halfway there, perhaps. But it wouldn't be as neat.)