Для кого-то мир, в котором выкидывать исключения из любого места программы - единственный и безальтернативный. Очень часто встречаются люди, которые, так сказать «брызжут слюной» отстаивая этот подход, даже не взглянув на альтернативы...
Простой пример, допустим давайте возьмем язык программирования C#, мы используем какую-то библиотеку и вызываем какой-то метод который возвращает тип A, если владелец библиотеки при обновлении своего детища решит заменить тип на B, мы же, при поднятии версии библиотеки увидим ошибки компиляции, и соответственно, обдумаем изменение в коде для каждого такого красного восклицательного знака. Вы скажете, это очень правильно и разумно, ведь другой тип и бла бла бла...
А теперь давайте расширим этот пример чуть-чуть, владелец библиотеки добавил исключение для этого метода, мы подняли версию, компилятор нам сказал как результат: «Братан, хорош, давай, давай, вперёд! Контент в кайф, можно ещё? Вообще красавчик! Можно вот этого вот почаще?», и компиляция прошла успешно. Странно, но адепты исключений говорят, что это норма, при этом противореча поинту про тип, ведь это можно сказать, такое же изменение типа возвращаемого значения.
Кроме этого всего, типы исключений которые выкидываются, будет невероятно хорошо, если хоть описаны в комментарии к методу, ведь язык не позволяет описывать что может быть выброшено (а может вообще ничего). И если не обработано на уровне выше, то летит еще дальше, и потом получается что-то типо такого?:
Я начинал программировать с языка PHP, когда в нем еще не добавили хоть какую-то типизацию (указание типов). И из-за незнания альтернатив, отстаивал явные проблемы языка, и когда начали появляться типы (указание типов), начал задумываться... Дальше был С#, с уже действительно строгой типизацией, и умные мысли начали меня догонять, но я был быстрее. И ВНИМАНИЕ, язык Swift, с еще более строгой типизацией, и двумя вариантами обработки ошибок прямо из коробки.
Так какой второй вариант обработки ошибок?
Вот:
enum Result<Success, Failure: Error> { case success(Success) case failure(Failure) }
enum TryMeBitchError: Error {
case myBoi case notMyBoi
}
func tryMeBitch() -> Result<Int, TryMeBitchError> {
// wat?
}
Но язык должен поддерживать достаточно прокаченные дженерики и ассоциированные значения в enum, иначе нужно хитрить. Но, честно говоря, я уже даже и не рассматриваю языки со строгой типизацией без всех трех этих пунктов (дженерики, ассоциированные значения в enum, выбор между подходами в обработке ошибок).
Такой подход закрывает все проблемы которые были приведены выше в примере с библиотекой, каждое изменение сигнатуры метода или типа внутри Result, будет заставлять нас обдумывать места вызова, тем не менее, порождает другие «особенности»... Например, бойлерплейт код, обрабатывать такие ошибки сложнее и не будет просто прокинуть ее до самого верха вызова, если такое нужно, но как показывает мой опыт, такое нужно не часто, а если и нужно, лучше я напишу бойлерплейт код, чем буду кастовать бубен и искать что происходит, при том что компилятор бы уже мог меня предупредить...
Вообще мой подход прост: «все что может отлавливать компилятор - отлавливаем». По этой причине рекомендую посмотреть на язык Rust и как можно еще более сильно углубиться в тему компилятора и того, что можно отследить на этапе сборки.
Я зашел в опасную зону из разряда iPhone или Android. Так что, пожалуйста, воспринимайте это все, как сугубо МОЕ личное мнение, а не посыл к действию. И описанное в этой статье, не является полной глубиной моих мыслей по этому вопросу. Плюсы есть и у старого доброго подхода с исключениями, но цель статьи было просто показать альтернативу. Все основано на моем опыте использования двух подходов.