Вопрос: Как правильно обрабатывать ошибки в C#?

Лучший ответ: Т.е. нужна некая философия
А какие книги вы уже пробовали читать, раз такое спрашиваете?
Ведь философия очень простая. Идея обработки ошибок с помощью механизма исключений сводится к тому, чтобы не захламлять основной алгоритм логикой обработки ошибок и иметь возможность локализовать эту обработку в одном месте, причем не где-нибудь, а именно там, где известно, как ошибку обработать.
Использование механизма исключений вместе с ООП-шной иерархической классификацией дает возможность обобщать исключения различной природы, если код их обработки также общий и не вдается в детали ошибки.
Все это дает вам набор простых правил:
1) выбрасывать исключение нужно тогда, когда вы не собираетесь обрабатывать возникшую ситуацию в рамках текущего алгоритма. Иными словами, для работающей в данный момент функции эта ситуация - "исключительная". Пример: вы пишете функцию для чтения GIF-файла в Bitmap, и по ходу чтения проверяете соответствие получаемых данных формату GIF-файлов (например, убеждаетесь в наличии GIF89a в начале файла). Если вдруг вы видите, что формат файла нарушен, то вам ничего не остается кроме как выбросить исключение, т.к. вы не можете продолжить выполнение основного алгоритма - считывание файла. Внутри функции считывания вы не знаете заранее, как вызывающий код захочет обработать эту проблему, вам и не нужно это знать;
2) перехватывать исключение конкретного типа нужно тогда, когда в задачу текущего кода входит и обработка ошибок тоже. Иными словами тогда, когда исключение в вызванном коде - лишь один из вариантов нормальной работы вызывающего кода. Вернемся к примеру с GIFками: если для библиотечной функции чтения файла нарушение формата - это критическая проблема, то для вызывающего эту функцию GUI-приложения это нормальная ситуация - ее можно и нужно обработать, выдав пользователю соответствующее сообщение, или просто пропустить файл, если обрабатывается сразу несколько картинок. Или например, если вы пишете веб-сервис, вы врядли хотите, чтобы весь сервис прекратил работу из-за ошибки при обработке какого-то одного запроса. Поэтому в веб-сервере, раздающем файлы вы, к примеру, можете перехватывать все FileNotFound исключения, и выдывать ошибку 404, а на все остальные исключения внутри обработчика запроса - ошибку 500 и в обоих случаях писать в error.log.
Следует понимать, что исключения - лишь один из подходов к обработке ошибок, естественно сочетающийся с возможностями некоторых языков. В Си, например, обходятся без них, и все живы и здоровы.
Отличный пример разных подходов - методы Parse/TryParse у числовых типов в дотнете. Первый возвращает значение и выбрасывает исключение, второй записывает значение через выходной параметр, возвращает bool и НЕ выбрасывает исключение. "Try" в названии второго метода подчеркивает, что для этого метода неудача при попытке распарсить число из строки - НОРМАЛЬНАЯ ситуация, и метод в этом случае вернет false. Для метода Parse напротив, такая ситуация будет исключительной, т.к. ему просто-напросто нечего будет возвращать, и дальнейшая нормальная работа кода, в том числе вызывающего, невозможна.
Поэтому метода TryParse чаще используют тогда, когда вероятность ошибки высока и ее обработка - одна из ветвей алгоритма. Например при считывании пользовательского ввода мы сразу можем попросить пользователя исправить введенное значение.
С другой стороны, Parse применяется если ошибка маловероятна, мы не готовы ее обработать и лучше прервать всю операцию целиком. Например если мы получили от сервера невалидный ответ, мы не попросим его исправить этот ответ. Дальнейшее общение с сервером лучше прервать, т.к. имеет место нарушение протокола и можно наломать дров.

Худший ответ: На эту тему не мало копий сломано, вплоть до полного отказа от конструкции try/catch/finally как от неявного goto. В собственном коде я свожу возможность генерации исключения к минимуму и каждый случай обсуждаем с лидом или архитектором - нужно оно там или нет