Здравствуйте, hi_octane, Вы писали:
_>Самый очевидный пункт: допустим у нас есть сериализатор, который пишет в абстрактный Stream. Этот Stream может быть диском, файлом, памятью, и т.п. Какие исключения объявить сериализатору, если он сам не знает, какой Stream ему дадут на вход? Заставить все Stream кидать абстрактный StreamWriteException?
Конечно StreamWriteException — это очевидно, причем во внутрь в innerException уже вкладывают то что породило данное исключение. В этом и есть смена акцентуации.
На более низком уровне — это может быть проблема с диском или обрыв подключения — но в данном контексте это вторично (т.е. в inner).
_>А почему совершенно не имеющие отношения к сериализатору классы, вдруг должны его учитывать и от него зависить, причём зависеть неформально?
Не должны зависеть — они могут выбрасывать наследника StreamExceptin, а сериализатор перехватывать все наследники и пихать в innerException своего StreamWriteException.
_>И это только один из сценариев.
Этот сценарий я разобрал и показал как правильно делать, давайте далее.
_> А есть ещё версии (например NetworkStream может начать поддерживать новый протокол, и кидать новые типы исключений), и так далее.
Слышали ли вы что-нибудь про наследование?
=сначала спроси у GPT=
Re[4]: Result objects - все-таки победили Exceptions?
_>И вопрос 5-летней давности всё ещё актуален — кто-то пробовал замерять, сколько сейчас все эти проверки жрут времени по сравнению с кодом без проверок но с исключениями?
Я, я! Правда, на Erlang'е, но сути дела не меняет.
С исключениями в среднем получается быстрее. Но при условии, что код писал разумный человек, и исключения бросались именно в исключительных ситуация, а не как часть control flow.
И заодно куда более читабельно. Без этой компиляторной магии с вопросительными значками, без самодельного создания stack trace, без двух десятков записей в лог (типа "не шмогла и на этом уровне, пробрасываю выше"). В общем, это правильное решение реальной задачи. Что бы там Растаманы не делали, а все равно в итоге получатся исключения. С квадратными ли, или восьмиугольными колесами — это уж как повезет. ИЧСХ, я уверен, что если колеса будет квадратными, это будет подано как фича: машине с такими колесами не нужен ручной тормоз, даже если парковаться на уклоне. И трогаться на уклоне без проблем, машина не откатится!
PS: читаю hi_octane, и прямо бальзам на душу. Я примерно лет 10 на этом форуме (и некоторых других, а также в тех компаниях, где успел поработать) высказываюсь аналогично. И казалось бы, ведь в FAANG должны быть семи пядей во лбу. Должны понимать такие вещи и без меня. Ан нет, даже 5 лет назад споры все еще случались, так что приходилось писать гайды на тему "почему bubble wrapping — плохо". Вообще интересно, кто все еще там работает на Erlang, остались ли все те гайды, что я писал, в силе, или кому-то нужно было нанести импакт, и что-нибудь поменяли.
Re[5]: Result objects - все-таки победили Exceptions?
S>На более низком уровне — это может быть проблема с диском или обрыв подключения — но в данном контексте это вторично (т.е. в inner).
Так проблема в том, что лазить по дебрям Inner Exceptions неправильно. В какой-то день писатель сериализатора захочет разделить его на 2 компонента — собственно писатель в стрим, и форматтер. И бах — у нас уже иерархия из 2-х Inner Exceptions, потому что каждый на своём уровне что-то во что-то завернул. Или для скорости форматтер теперь многопоточный — и у нас InnerException стал AggregateException. К любому такому сюрпризу вызывающий код готов не будет. Проверит что в InnerException неведомая хрень, и либо сломается от неожиданности там где ломаться не надо, либо проигнорит.
S>Не должны зависеть — они могут выбрасывать наследника StreamExceptin, а сериализатор перехватывать все наследники и пихать в innerException своего StreamWriteException.
InnerException это вообще штука информативная только для разработчика на работе. Чтобы разбирать тихонько лог, и думать что сделать чтобы так падало пореже. Если внешний код серьёзно начинает полагаться на разбор InnerException — беда неминуема.
S>Этот сценарий я разобрал и показал как правильно делать, давайте далее.
Только один нюанс — если мы передаём сериализатору абстрактный Stream, который может кидать любые невиданные ранее исключения, а заворачивает их в StreamWriteException сам сериализатор, то все объявления в секции checked у Stream не имеют смысла. Потому что сериализатору в общем-то пофиг что там, он всё равно с этим сделать ничего не может, и вынужден заворачивать в свою обёртку всё, что бы там ни было. И сериализатор тут только простой наглядный пример. Так-то во всех местах где один код вызывает другой, и есть какая-то иерархия классов или интерфейсов — секция checked начинает вот так размазываться до бесполезной. Что и произошло с Java.
S>Слышали ли вы что-нибудь про наследование?
Слышал. Вот есть базовый Stream, у него checked фиксированный набор исключений. И вдруг появляется новый NetworkStream, которому надо добавить новый CertificateException. А добавить низзя: базовый стрим менять не положено — на его checked уже весь клиентский код завязался. У пользователей уже свои checked зависят от тех checked что были у старого Stream. Приехали. В Java это проблему решили "элегантно" — забили на checked. Могли бы иначе — решили бы иначе.
Re[5]: Result objects - все-таки победили Exceptions?
S>Конечно StreamWriteException — это очевидно, причем во внутрь в innerException уже вкладывают то что породило данное исключение. В этом и есть смена акцентуации.
С этими "вложениями исключений" нужно быть крайне аккуратным. Потому что при бездумном использовании у нас опять получится call stack, чего стоит избегать. На мой взгляд, оборачивать исключение в другое исключение имеет смысл только в том случае, когда эта обертка добавляет контекст, недоступный в call stack (скажем, какие-то значения local variables, или что-то из global mutable state).
И дело там вовсе не в "акцентуации", а в том, чтобы собрать полный контекст (а значит, иметь возможность воспроизвести проблему путем создания аналогичного контекста). Задача заметно упрощается, если функции — чистые (pure functions), тогда исключение может пролететь сколько угодно фреймов стека, и по дороге собирая весь контекст автоматически.
Re[3]: Result objects - все-таки победили Exceptions?
Здравствуйте, Shmj, Вы писали:
vsb>>Считаю, что непроверяемые исключения в общем случае это идеальный способ обработки ошибок и все эти result objects не нужны (могут быть нужны в специфических случаях, но не в общем случае). Проверяемые исключения тоже не нужны, от них проблем больше, чем пользы. По крайней мере в том виде, в котором они в Java.
S>А какие проблемы с ними?
Да много проблем. Их никто не использует, думаю, это достаточный индикатор того, что что-то с ними не так. У тебя методы превращаются в длиннющие throws из десятков исклюючений. Ты добавляешь в одну реализацию работу с XML и интерфейс должен кидать это исключение, все клиенты сломаны, теперь это XML исключение должно бросаться изо всех клиентов этого интерфейса.
Или на каждый класс надо создавать отдельное исключение, а то и на каждый метод, заворачивающий вложенное исключение. И весь смысл этой типизации теряется кроме тонн лишней писанины.
Я не отрицаю некоторого смысла во всей этой идее проверяемых исключений, но конкретно в Java реализация фатально плохая. И я не уверен, что знаю, как сделать лучше.
Если немного пересмотреть всю эту концепцию, можно прийти к аналогии со статической и динамической типизацией.
Думаю, сегодня уже никто не будет отрицать, что и языки с динамической типизацией и языки со статической типизацией имеют свои плюсы и минусы. При этом тем же Haskell-ем, который можно считать эдакой кульминацией статической типизации, мало кто пользуется, не в последнюю очередь из-за сложности. Больше пользуются языками, которые в основном статически типизированы, но в отдельных местах не гнушаются динамической типизации. Нет ничего криминального в том, чтобы передать значение, как Object и потом проверить его через instanceof. Ну не проверит компилятор это место, ну и ладно, переживём.
И вот в этой перспективе проверяемые и непроверяемые исключения можно сравнить со статически и динамически типизированными языками программирования. Да, непроверяемые исключения это, безусловно, обход типизации языка. Да, проверяемые исключения в теории позволят найти какой-то баг, когда программист был обязан обработать какое-то исключение. Но они же привносят неудобства, которые на практике слишком велики. А с непроверяемыми исключениями всё очень просто и понятно.
Re[4]: Result objects - все-таки победили Exceptions?
Здравствуйте, vsb, Вы писали:
vsb>Я не отрицаю некоторого смысла во всей этой идее проверяемых исключений, но конкретно в Java реализация фатально плохая. И я не уверен, что знаю, как сделать лучше.
Основная проблема не в исключениях самих по себе, а в том, что они слишком "другие" и плохо совмещаются с другими фичами ЯП.
Например, был у тебя for-цикл, решил переписать на stream с лямбдами и checked исключения, внезапно, через лямбду уже не передать.
Распараллелить кусок кода, и вот теперь у тебя проблема как пробрасывать исключения между тредами и что с ними делать потом.
В зависимости от контекста одна и та же операция может иметь разную "серьёзность". parseInt в каком-то участке кода — это жуткая проблема и надо сразу падать, а где-то это норма и требуется быстро парсить в цикле. В итоге приходится два варианта функций писать: parseInt throws и tryParseInt с кодом возврата.
С популяризацией подходов функционального программирования с исключениями чаще приходится бороться, чем извлекать пользу.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[6]: Result objects - все-таки победили Exceptions?
Здравствуйте, hi_octane, Вы писали:
_>Так проблема в том, что лазить по дебрям Inner Exceptions неправильно. В какой-то день писатель сериализатора захочет разделить его на 2 компонента — собственно писатель в стрим, и форматтер. И бах — у нас уже иерархия из 2-х Inner Exceptions, потому что каждый на своём уровне что-то во что-то завернул. Или для скорости форматтер теперь многопоточный — и у нас InnerException стал AggregateException. К любому такому сюрпризу вызывающий код готов не будет. Проверит что в InnerException неведомая хрень, и либо сломается от неожиданности там где ломаться не надо, либо проигнорит.
Вызывающий код знает только что сериализация не удалась. Зачем ему знать больше? Если нужно больше для отладки — добро пожаловать в inner.
S>>Не должны зависеть — они могут выбрасывать наследника StreamExceptin, а сериализатор перехватывать все наследники и пихать в innerException своего StreamWriteException. _>InnerException это вообще штука информативная только для разработчика на работе. Чтобы разбирать тихонько лог, и думать что сделать чтобы так падало пореже. Если внешний код серьёзно начинает полагаться на разбор InnerException — беда неминуема.
Почему же — вполне нормальная практика — пройтись по всей иерархии и найти первопричину.
S>>Этот сценарий я разобрал и показал как правильно делать, давайте далее. _>Только один нюанс — если мы передаём сериализатору абстрактный Stream, который может кидать любые невиданные ранее исключения, а заворачивает их в StreamWriteException сам сериализатор, то все объявления в секции checked у Stream не имеют смысла. Потому что сериализатору в общем-то пофиг что там, он всё равно с этим сделать ничего не может, и вынужден заворачивать в свою обёртку всё, что бы там ни было. И сериализатор тут только простой наглядный пример. Так-то во всех местах где один код вызывает другой, и есть какая-то иерархия классов или интерфейсов — секция checked начинает вот так размазываться до бесполезной. Что и произошло с Java.
Stream — это контракт. Проверяемые исключения — так же часть контракта, они должны быть не любыми а только наследниками StreamException.
S>>Слышали ли вы что-нибудь про наследование? _>Слышал. Вот есть базовый Stream, у него checked фиксированный набор исключений. И вдруг появляется новый NetworkStream, которому надо добавить новый CertificateException. А добавить низзя: базовый стрим менять не положено — на его checked уже весь клиентский код завязался. У пользователей уже свои checked зависят от тех checked что были у старого Stream. Приехали. В Java это проблему решили "элегантно" — забили на checked. Могли бы иначе — решили бы иначе.
Так сделайте наследника StreamException — в чем вопрос
=сначала спроси у GPT=
Re[6]: Result objects - все-таки победили Exceptions?
Здравствуйте, SkyDance, Вы писали:
SD>С этими "вложениями исключений" нужно быть крайне аккуратным. Потому что при бездумном использовании у нас опять получится call stack, чего стоит избегать. На мой взгляд, оборачивать исключение в другое исключение имеет смысл только в том случае, когда эта обертка добавляет контекст, недоступный в call stack (скажем, какие-то значения local variables, или что-то из global mutable state).
Со всем нужно работать обдуманно, испортить можно любую идею.
=сначала спроси у GPT=
Re[4]: Result objects - все-таки победили Exceptions?
Здравствуйте, vsb, Вы писали:
vsb>Да много проблем. Их никто не использует, думаю, это достаточный индикатор того, что что-то с ними не так. У тебя методы превращаются в длиннющие throws из десятков исклюючений. Ты добавляешь в одну реализацию работу с XML и интерфейс должен кидать это исключение, все клиенты сломаны, теперь это XML исключение должно бросаться изо всех клиентов этого интерфейса.
Не все поняли как правильно пользоваться. Исключение — это часть конракта. Если контракт не изменялся — то новое исключение не добавляете, просто пишите в inner того исключения, которое является частью контракта. Выше приводили пример с сериализацией — SerializationException — а внутри уже inner. Сериализатор перехватывает только наследников StreamException.
vsb>Или на каждый класс надо создавать отдельное исключение, а то и на каждый метод, заворачивающий вложенное исключение. И весь смысл этой типизации теряется кроме тонн лишней писанины.
Там где не нужна акцентуация — просто оборачиваете в RuntimeException — не проверяемое.
vsb>Я не отрицаю некоторого смысла во всей этой идее проверяемых исключений, но конкретно в Java реализация фатально плохая. И я не уверен, что знаю, как сделать лучше.
Просто не поняли идею.
vsb>Думаю, сегодня уже никто не будет отрицать, что и языки с динамической типизацией и языки со статической типизацией имеют свои плюсы и минусы. При этом тем же Haskell-ем, который можно считать эдакой кульминацией статической типизации, мало кто пользуется, не в последнюю очередь из-за сложности. Больше пользуются языками, которые в основном статически типизированы, но в отдельных местах не гнушаются динамической типизации. Нет ничего криминального в том, чтобы передать значение, как Object и потом проверить его через instanceof. Ну не проверит компилятор это место, ну и ладно, переживём.
Возможно дело не в сложности — а в непривычности. И в том что он сырой еще, над ним мало работали. Возможно лет через 50 человечество до него дорастет.
vsb>И вот в этой перспективе проверяемые и непроверяемые исключения можно сравнить со статически и динамически типизированными языками программирования. Да, непроверяемые исключения это, безусловно, обход типизации языка. Да, проверяемые исключения в теории позволят найти какой-то баг, когда программист был обязан обработать какое-то исключение. Но они же привносят неудобства, которые на практике слишком велики. А с непроверяемыми исключениями всё очень просто и понятно.
Просто люди не проработали в голове концепцию и как бы боятся исключений. Привыкли терять контроль над кодом. У меня каждый метод КАЖДЫЙ — я 100% знаю не только параметры, но и все возможные исключения, которые он может выбросить. Т.е. 100% контроль кода. А люди привыкли к исключениям относиться как к магии. Как бы какая-то неприятность, которой быть не должно.
=сначала спроси у GPT=
Re[4]: Result objects - все-таки победили Exceptions?
Здравствуйте, hi_octane, Вы писали:
S>>А какие проблемы с ними? _>Ищи статьи или интервью Хельсберга. Они в MS очень долго решали, потому что сначала идея checked exceptions им тоже казалась очень правильной.
ну там мне кажется довольно просто. Они просто прогнулись под мнением большинства. Тогда слишком сильно приоткрыли окно Овертона и многие разработчики (в том числе и я) восприняли идею в штыки. Например на Rust и Go уже ругаются куда меньше. Хотя там всё куда извращённее.
_>Самый очевидный пункт: допустим у нас есть сериализатор, который пишет в абстрактный Stream. Этот Stream может быть диском, файлом, памятью, и т.п. Какие исключения объявить сериализатору, если он сам не знает, какой Stream ему дадут на вход? Заставить все Stream кидать абстрактный StreamWriteException? А почему совершенно не имеющие отношения к сериализатору классы, вдруг должны его учитывать и от него зависить, причём зависеть неформально?
а почему не должны? Мы должны зависеть от абстракций, но не от реализаций. StreamWriteException — абстракция. Не вижу здесь проблемы. Если клиент не работает с сериализатором напрямую, пусть принимает на вход ещё более общую ошибку: IOError н-р.
И вообще не ясно, как решают проблему непроверяемые исключения? Добавилась у вас новая ошибка MySpecificStreamError. Кто-то её перехватит? Или подождут релиза когда у клиента вылетит необработанное? Вот как-то не помню чтобы я сильно мучался с проверяемыми исключениями в java (было это правда давно), зато отчётливо помню как напарывался на необработанные исключения.
Re[4]: Result objects - все-таки победили Exceptions?
Здравствуйте, hi_octane, Вы писали:
AD>>Коды ошибок в расте? Интересно... _>Если речь о том что Option или Variant сильно отличаются от return 0 в C, или HRESULT в COM/OLE (тут серая рожа того кто знает ) — то для меня разница исчезающе мала. Современными средствами можно накрутить на почти любой язык проверку что на каждый возврат из функции написан if. То что в Rust такая проверка часть языка — просто деталь реализации.
А какая разница, осилили они или неосилили исключения? Какая разница что оператор ? — это просто синтаксический сахар? Лично мне нравится подход с ? тем, что визуально просматривая код, ты всегда видишь где ошибка передаётся на уровень выше.
Подобные споры — это не более чем споры о вкусах. В разных проектах на разных языках я использую и исключения и result values. И меня устраивает и то и другое.
Здравствуйте, ·, Вы писали:
vsb>>Я не отрицаю некоторого смысла во всей этой идее проверяемых исключений, но конкретно в Java реализация фатально плохая. И я не уверен, что знаю, как сделать лучше. ·>Основная проблема не в исключениях самих по себе, а в том, что они слишком "другие" и плохо совмещаются с другими фичами ЯП.
·>Например, был у тебя for-цикл, решил переписать на stream с лямбдами и checked исключения, внезапно, через лямбду уже не передать.
·>Распараллелить кусок кода, и вот теперь у тебя проблема как пробрасывать исключения между тредами и что с ними делать потом.
Проверяемые исключения были плохой идеей до лямбд.
В принципе я не вижу ничего, что мешало бы сделать лямбды с нормальными проверяемыми исключениями. Немного язык доработать пришлось бы, но ничего сверхестественного. И сейчас можно писать interface Runnable<E> { void run() throws E; }, но это работает только с одним исключением. Нужно было сделать непредставимый тип вроде SQLException | IOException, который бы присваивался типу E. Ну и спроектировать стримы с учётом этой фичи. И было бы нормально. Но это так, к слову.
Re[5]: Result objects - все-таки победили Exceptions?
Здравствуйте, Shmj, Вы писали:
S>Просто люди не проработали в голове концепцию и как бы боятся исключений. Привыкли терять контроль над кодом. У меня каждый метод КАЖДЫЙ — я 100% знаю не только параметры, но и все возможные исключения, которые он может выбросить. Т.е. 100% контроль кода. А люди привыкли к исключениям относиться как к магии. Как бы какая-то неприятность, которой быть не должно.
Во-первых, если говорить про Java, каждый метод может бросать StackOverflowError при вызове.
Во-вторых 99% методов могут бросать OutOfMemoryError при использовании new или вызове других методов, использующих new.
Это просто штатно, альтернатива разве что ронять JVM вместо выброса таких исключений, но это уже совсем глупо.
Точно так же 99% методов могут бросать разные другие исключения. К примеру вызываем метод, его байткод не загружен в JVM. Грузим байткод, диск сломался и бросил IOException при попытке чтения JAR файла. Кидается IOException обёрнутый в какой-то Error. Это всё теоретически возможно.
Мы логгируем что-то в stdout и может броситься ошибка.
Т.е. концепция полного отказа от непроверяемых исключений не выдерживает никакой критики. Всегда будет множество ошибок, которые в теории могут возникать, но которые 100% неразумно включать в контракт функции.
Здравствуйте, Shmj, Вы писали:
S>Но, как оказалось, народ идеи не понял. Так и вернулись к понятным дебилоидам кодам возврата, т.к. проверяемые исключения осилить не смогли. Так же и в Kotlin их решили не делать.
На деле, индустрия развивается в сторону писать как можно меньше кода. В случае с Java — проверяемые исключения это именно что ошибка языка, а особенно то, что там половина стандартной библиотеки их кидает. Как результате, код получается весьма многословным, на практике приходится их чаще всего тупо преобразовывать в непроверяемые чтоб не городить кучу строк кода на ровном месте. И большинство тех, кто кроме Java никаких языков не знает, блин именно что городит кучу кода на ровном месте для простейших случаев. В Котлин именно что исправили это недоразумение, в результате не надо быть мегасеньером чтоб не городить лютейший говнокод на ровном месте, даже у типичного быдлокодера строчек становится на порядок меньше в результате.
В расте же с кодами ошибок — это полный отстой. Вроде жить можно и вроде как даже с этим всем сильно лучше чем в Java, но мне крайне не понравилось — грабли блин на ровном месте. Собственно это весьма неплохая причина малопопулярности раста, обработка ошибок там отстой, кто додумался еще по умолчанию кидать паник который вообще падает нафиг, это жуть, там вообще чтоль не предполагается что на раст будут писать что то больше тысячи строк?
Как на чистом Си народ без исключений живет, который не желает говнокодить лютейшие однотипные простыни на ровном месте — самому интересно. Макросами чтоль выкручиваются? Могли бы макросы соответствующие в стандартные либы запихнуть в этом случае и сделать бест практикой, вроде до чих пор такого нет. Проверять блин коды возврата это идиотизм полный, когда функция возвращает код возврата, а реальный результат по ссылке — это полный писец. Фигачить везде дополнительный аргумент типа ошибки, передавая ее по ссылке — и тоже явно проверять, хоть даже и макросом — тоже лютейший писец на ровном месте.
Потому звиняй, но исключения как были мейнстримом, так и остались. Есть конечно любители обработки ошибок через монады, но я вот сильно не уверен что это верный путь. Как минимум из за оверхеда на ровном месте, лютых стектрейсов, а также весьма специфической читабельности, крайне далекого от естественного языка. И вообще ИМХО монады и все такое — это тупо пока неизбежное зло для асинхронности в тех языках, где асинхронность не сделали на уровне синтаксиса языка, в случае async await и их более простых будущих аналогов не вижу смысла в коде на монадах.
Re: Result objects - все-таки победили Exceptions?
, что отличия настолько исчезающе малы, что их в лупу не разглядишь.
КБ>Когда вы уже поймете что ваш сишный опыт абсолютно уже не релевантен
Много разного опыта позволяет видеть недостатки любой штуки без розовых очков. Например, после scala sbt, на все остальные билд-системы без слёз не взглянешь. А если возвращаться к теме ошибок/исключений, то первый раз столкнувшись с трейтом std::Error в Rust, у меня, среди матюков, проскочила мысль что это было предопределено — миллениалы должны были придумать достойного соперника для HRESULT
Re[4]: Result objects - все-таки победили Exceptions?
Здравствуйте, vsb, Вы писали:
vsb>·>Распараллелить кусок кода, и вот теперь у тебя проблема как пробрасывать исключения между тредами и что с ними делать потом. vsb>Проверяемые исключения были плохой идеей до лямбд. vsb>В принципе я не вижу ничего, что мешало бы сделать лямбды с нормальными проверяемыми исключениями. Немного язык доработать пришлось бы, но ничего сверхестественного. И сейчас можно писать interface Runnable<E> { void run() throws E; }, но это работает только с одним исключением. Нужно было сделать непредставимый тип вроде SQLException | IOException, который бы присваивался типу E. Ну и спроектировать стримы с учётом этой фичи. И было бы нормально. Но это так, к слову.
Очень всё становится сложным и хитрым. Допустим, даже все исключения непроверяемые.
Было:
List<Result> doSomething(List<File> files) throws WhatExceptions??
{
var results = new ArrayList<Result>();
for(var f : files)
{
var data = readData(f);
var parsed = parseData(data);
if(!good(parsed)) continue;
var result = process(parsed);
results.add(result);
}
}
S>Так сделайте наследника StreamException — в чем вопрос
Ага, и тут у разработчиков менеджера сертификатов дилемма — делать CertificateException наследником StreamException, или HTTPException, или послать всех нафиг
При правильном пробросе нижележащих исключений наверх, вызывающий код ловит те исключения, которые к нему реально относятся, и просто их обрабатывает. Заворачивание в обёртки имеет смысл только если вызывающий код абсолютно гарантированно должен быть абстрагирован от деталей.
На примере сериализатора — если вызывающий код сам же передаёт Stream в сериализатор, то вызывающий код вполне может (и даже должен) перехватывать исключения прилетающие от своего собственного Stream. А сериализатор может (и даже должен) пробрасывать эти "чужеродные" исключения наверх. Он их всё равно обработать не может, там снаружи лучше знают что с делать, если вылетел StreamVeryTiredException.
А вот если, на этом же примере, сериализатор внутри создаёт свой собственный BufferedMemoryStream, в который пишет временные данные, а потом сбрасывает эти данные в Stream переданный извне, то внутренние ошибки этого BufferedMemoryStream ни в коем случае не должны "утекать" наверх. Потому что снаружи никто про этот BufferedMemoryStream не знает, и знать не может. В этом случае наружу должен улетать SerializationException (даже не StreamWriteException, потому исключения в этом буфере такая же внутренняя деталь реализации).