The problem is that when exceptions are available, people will start using them for non-exceptional things and (the worst case) basic control flow.
Case in point from a popular framework, Django: the standard way of getting an object from the DB via the ORM is Class.objects.get. This method either returns a single object or raises on exception if there are zero rows in the db or another exception if there are two or more rows. It may also raise other kinds of exceptions.
Now, it's clear that having zero rows is not very exceptional, and even >1 is somewhat debatable. Note especially that this is not just some weekend project by a nobody, it is a framework that is widely used and respected.
Except that get accepts as parameters non-unique non-primary fields too.
I have every reason to believe that the Django devs are capable people who thought about this and landed on this specific usage with a clear rationale... but it still is a misuse of error handling capabilities of a language. And a sort of misuse that everyone else does sometimes as well.
I agree with your first paragraph but not with the Django example. User.objects.get(id=1) failing is exceptional because you are asking for an object with a specific and unique property. Otherwise, you would be using User.objects.filter(age>20) (which doesn't throw an exception).
I might kinda agree that exact queries against primary keys not having results (or having more than 1) would be exceptional, but this actually happens for every other field as well.
Sometimes you want non-local transfers of control, beyond just exceptions. Python uses exceptions as both error conditions and signals.
It's seemingly nearly mandatory in discussions regarding exceptions to bring up Common Lisps's condition system, which is a superset of exceptions. I've included a link to a chapter from a book on Lisp.
that's not a Django problem; it's a Python problem. Using exceptions for control flow is idiomatic in Python. Case in point: a generator is supposed to raise a StopIteration exception in order to signal termination.
Case in point from a popular framework, Django: the standard way of getting an object from the DB via the ORM is Class.objects.get. This method either returns a single object or raises on exception if there are zero rows in the db or another exception if there are two or more rows. It may also raise other kinds of exceptions.
Now, it's clear that having zero rows is not very exceptional, and even >1 is somewhat debatable. Note especially that this is not just some weekend project by a nobody, it is a framework that is widely used and respected.