Exceptions work excellently for business-level server software; typically they make the server transaction all or nothing, with transactional semantics. The stateless nature of HTTP, and transactions provided by the database, usually add up to make this easy. Business logic throws an exception, everything unwinds, the transaction rolls back, no state was modified, and the server returns an error code to the client: all good.
When you need partial updates to state in the case of an error, then you're in trouble. When you have multiple transactions to coordinate, but no distributed transaction coordinator, you're in trouble (but you are anyway whether you use exceptions or not). The more state you need to preserve in an error case, the worse exceptions are for your use case. Ideally, exceptions are never caught except at the topmost loop: server request dispatcher, UI event handler, or command line driver. When execution must continue with recovery, exceptions are an inferior tool.
A http request handler is not what I mean by server software. I mean a real server - a long-running process with complex state. Yes, exceptions work for simple programs and short scripts (where you don't handle them at all), or as a plug-in mechanism where there is a catch-all handler at the boundary. I kind-of value exceptions as well for writing python scripts.
To a first approximation, most servers with distinct codebases are HTTP app servers ("request handlers").
And not just simple programs and short scripts; very complex programs, as long as they keep state in a transactional store, and a request has approximately the same lifetime as the transaction.
Sure, if you want to think of the thing that is formed by all combined requests from all clients over the lifetime of the database, as a program - then that might (or might not) be a complex program. But the unit in which exceptions are confined with this architecture is the simple, dirty request handler script.
Which likely doesn't even try to handle any errors, and may actually be a bad, faulty program - the edge cases just don't matter that much for businesses. Maybe an e-mail will be sent out twice in a few circumstances, etc: nobody cares.
Transaction management is implemented in the database server, and very likely that doesn't employ exceptions. And more complexity is implemented by the OS (the "process server" if you will) - if your program dies it specifies how to handle the condition, etc. Another part of complexity is dealt with by the language runtime, for example garbage collection - no exceptions to be seen there.
> Sure, if you want to think of the thing that is formed by all combined requests from all clients over the lifetime of the database, as a program
The thing that serves responses to requests is a program. And it's not just a "simple, dirty request handler script," it's often enormously complex. Entire companies exist with their product exposed via HTTP APIs.
The reason you don't see exceptions in databases, operating systems, etc is because they're often written in C++, which has notoriously wonky semantics around exceptions, or C, which doesn't have exceptions at all.
When you need partial updates to state in the case of an error, then you're in trouble. When you have multiple transactions to coordinate, but no distributed transaction coordinator, you're in trouble (but you are anyway whether you use exceptions or not). The more state you need to preserve in an error case, the worse exceptions are for your use case. Ideally, exceptions are never caught except at the topmost loop: server request dispatcher, UI event handler, or command line driver. When execution must continue with recovery, exceptions are an inferior tool.