Очень часто программисты не думают о чрезвычайных ситуациях. Особенно печально, когда это не разработчики приложений, а авторы библиотек и фреймворков.

Например, когда я готовил этот материал, я спросил в чате 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 у нас уже есть нормализация с асинхронными функциями: что бы мы ни возвращали или кидали, они всегда возвращают промис. А промисы, кстати, в нашем случае нужно не возвращать, а выбрасывать, но об этом в другой раз.

А пока, подписывайтесь на что-нибудь, вступайте во что-то там, и держите руку на пульсе вот этого вот.

Комментарии (2)


  1. dimazizinoviev
    19.06.2026 09:01

    В реальных системах нельзя делать вид, что ошибок не существует. Они будут всегда, вопрос только в том, как система с ними живёт: падает, замирает или продолжает работать частично. Мне ближе подход, где ошибка не рушит всё приложение, а становится частью состояния и нормально обрабатывается.


    1. cmyser
      19.06.2026 09:01

      ровно этот подход и описан в статье