Очень часто программисты не думают о чрезвычайных ситуациях. Особенно печально, когда это не разработчики приложений, а авторы библиотек и фреймворков.
Например, когда я готовил этот материал, я спросил в чате Effector, как система ведет себя при возникновении исключений. На что мне ответили, что в чистых функциях не должно быть исключений (кстати, исключения на самом деле не противоречат чистоте, но это другая история), и если ты их допустил, то ты сам дурак. Когда я спросил, знает ли автор, как его библиотека поведет себя в чрезвычайной ситуации, меня обвинили в токсичности и забанили.
Ошибок тоже не должно быть, как и других неприятностей в жизни, однако они иногда случаются. Вина ли это программиста, браузера, его расширений, операционной системы, звездного ветра — неважно. Нужно уметь принимать удар и не прятать голову в песок. Но в разных реактивных библиотеках мы столкнемся с разным поведением…
? Unstable: Нестабильная работа
⛔ Stop: Остановка работы
? Revert: Откат к стабильному состоянию
? Store: Индикация ошибки и ожидание восстановления
? Unstable: Нестабильная реактивность
Часто при возникновении исключения приложение переходит в неконсистентное состояние, что приводит к нестабильной работе.

В примере, допустим, в имя закрался некорректный codepoint. И, скажем, попытка взять длину строки в этом случае приводит к исключению. Пример довольно синтетический, позже я покажу более реалистичные, а пока так.
Итак, при вычислении инварианта произошло исключение, из-за чего runtime не обновил Count. В результате все состояния разделились на 2 подграфа, которые сами по себе консистентны, но уже не согласованы друг с другом.
⛔ Stop: Остановка реактивности
Не менее странное решение — просто прекратить работу, как, например, делает RxJS. Если в потоке где-то возникает исключение, то все последующие потоки завершаются и больше не работают.

Эта стратегия подходит для одиночной задачи — либо ты ее выполняешь, либо падаешь с ошибкой. Но реактивность предназначена для долгоживущих систем, которые постоянно что-то отображают. Это значит, что если реактивность сломается, приложение просто сломается без уведомления пользователя.
Более того, оно сломается только наполовину. А чтобы восстановить работу, придется перезапускать либо все приложение, либо хотя бы эту половину.
Случай из жизни: на моем этаже в отеле поселилась толпа спортсменов. И это ребята под 100 кг чистого мяса. И вот как-то утром мы втиснулись с ними в лифт, что предсказуемо привело к перегрузке. Лифт поднял лапки и сказал “все”.
Ну, ладно, пара вышла — ничего не произошло. Половина вышла — все равно ничего. Все вышли — лифт все равно не заработал. И нам всем пришлось бежать по лестнице. Думаю, софт для этого лифта писали на RxJS, не иначе.
? Revert: Откат к стабильному состоянию
Некоторые библиотеки, в принципе, не допускают неконсистентности, пересчитывая инварианты в рамках транзакции. Так что если что-то идёт не так, все состояния откатываются к последнему консистентному.

Формально звучит неплохо. Но для пользователя это ужасное поведение, потому что из-за одной дряной овцы где-то в углу приложения, которая постоянно выбрасывает исключения, все приложение встает колом и никак не реагирует на действия пользователя. Или проще говоря — оно крепко зависает. Что нехорошо.
? Store: Индикация ошибки и ожидание восстановления
Гораздо практичнее рассматривать ошибку как возможный результат вычисления наряду с возвращаемым значением.

Здесь все состояния, зависящие от некорректного, тоже помечаются как некорректные. И система рендеринга может автоматически показывать индикатор ошибки для частей приложения, которые не удалось обновить. Либо можно поймать исключение и нарисовать свое красивое сообщение. В любом случае пользователь поймет, что происходит и что на сломанную часть приложения полагаться не стоит. Но другие части можно продолжать использовать.
Хотя часть приложения сломана, состояние приложения остается консистентным. Потому что сообщение об ошибке на выходе согласовано с некорректным значением на входе.
При этом устранение причины сбоя автоматически восстановит корректную работу этой части приложения. Без дополнительных действий со стороны программиста!
Именно эта стратегия и используется в $mol_wire - самом продвинутом реактивном рантайме.
Исключения в $mol_wire
Реактивное состояние может принимать один из 3 возможных типов значений:
Result — фактическое валидное значение.
Error — информация об исключении.
Promise — обещание, что скоро появится либо значение (Result), либо объяснение, почему его нет (Error).
Эти значения могут поступать следующими способами:
return— значение, возвращаемое функцией.throw— прерывание выполнения.put— прямое запись значения в кэш.
В данном случае неважно, каким способом мы доставили значение — оно записывается в кэш. При доступе к состоянию поведение зависит только от типа значения:
Ошибки и промисы выбрасываются через
throw.Остальные значения возвращаются через
return.
class MyModel extends Object { // throws Promise or Error @act fetchJSON( url: string ): object { return fetch( url ).then( resp => res.json() ) // return Promise } // throws Promise @mem profile(): { name: string } { try { return this.fetchJSON( '/profile' ).user } catch( error ) { if( 'then' in error ) throw error // catch & throw Promise return { name: 'unknown' } } } // throws Promise @mem name(): string { return this.profile().name // bypass Promise } }
То есть реактивная мемоизация методов не совсем прозрачна: если вы вернете экземпляр Error, он все равно будет выброшен позже, а если вы выбросите, например, строку, она все равно будет возвращена позже. Получается своего рода нормализация поведения.
Конечно, можно было бы сделать прозрачное поведение, но в JS у нас уже есть нормализация с асинхронными функциями: что бы мы ни возвращали или кидали, они всегда возвращают промис. А промисы, кстати, в нашем случае нужно не возвращать, а выбрасывать, но об этом в другой раз.
А пока, подписывайтесь на что-нибудь, вступайте во что-то там, и держите руку на пульсе вот этого вот.
dimazizinoviev
В реальных системах нельзя делать вид, что ошибок не существует. Они будут всегда, вопрос только в том, как система с ними живёт: падает, замирает или продолжает работать частично. Мне ближе подход, где ошибка не рушит всё приложение, а становится частью состояния и нормально обрабатывается.
cmyser
ровно этот подход и описан в статье