Тут достаточно просто все.
Исключения помогают проскочить участок кода при выполнении определенных условий.
Причем, при коде без вызовов функций - всегда можно заменить на if/else, но код будет многовложенным (один if в другом). Но такой код естественно давно никто не пишет.
А при коде с вызовом функций это становится и вовсе невозможно (нельзя начало if написать в одном методе, а закрывающую скобку в другом). И нам приходится всю цепочку if передавать по стеку вызовов вверх.
Идея же исключений такая:
1. У нас есть алгоритм, который должен работать по заданной схеме. Мы нигде на уровне выше не проверяем корректность возвращаемых значений или правильность выполнения уровня ниже - он должен выполниться правильно или не выполниться. Это условие рождается из понимания инкапсуляции - каждый отвечает за свой кусок кода.
2. Если в какой момент момент, метод (кусок кода), отвечающий за определенную функциональность понимает, что не может выполнить назначенную ему операцию - он сообщает об этом на уровень выше.
3. Уровень выше может обработать исключительную ситуацию, либо (если не знает как) - передать исключение еще уровнем выше по стеку вызовов.
Т.е. резюмирую: у нас есть код, который должен в 90% случаев обрабатываться по одному алгоритму и в 10% случаях могут возникать ситуации, когда этот алгоритм в одной конкретной части кода - пойдет по другому сценарию.
Т.е. ваша задача писать код именно таким образом, чтобы исключения были лишь подстраховкой, а не частью основного алгоритма.
Интересный момент реализации исключений в lisp: там можно выполнить код вызвавший исключение повторно (например попытаться подключиться к базе второй раз средствами самого исключения).