Моя личная рекомендация такова: исключение выдается, когда фундаментальное предположение о текущем кодовом блоке оказывается ложным.
Пример 1: допустим, у меня есть функция, которая должна проверять произвольный класс и возвращать true, если этот класс наследует от List <>. Эта функция задает вопрос "Является ли этот объект потомком List?" Эта функция никогда не должна генерировать исключение, потому что в ее работе нет серых областей - каждый отдельный класс наследует или не наследует от List <>, поэтому ответ всегда «да» или «нет».
Пример 2: допустим, у меня есть другая функция, которая проверяет List <> и возвращает true, если его длина больше 50, и false, если длина меньше. Эта функция задает вопрос: «Есть ли в этом списке более 50 элементов?» Но этот вопрос делает предположение - он предполагает, что объект, который ему дан, является списком. Если я передаю ему значение NULL, то это предположение неверно. В этом случае, если функция возвращает либо true или false, то она нарушает свои собственные правила. Функция не может вернуть что-либо и утверждать, что она ответила на вопрос правильно. Так что он не возвращается - он выдает исключение.
Это сравнимо с "загруженным вопросом" логическая ошибка. Каждая функция задает вопрос. Если вводимые данные делают этот вопрос ошибочным, выведите исключение. Эту линию труднее нарисовать с помощью функций, возвращающих void, но суть в следующем: если предположения функции относительно ее входных данных нарушаются, она должна выдать исключение вместо нормального возврата.
Другая сторона этого уравнения: если вы обнаружите, что ваши функции часто генерируют исключения, вам, вероятно, нужно уточнить их допущения.