Exodus — это легковесная клиент-серверная библиотека для 1С:Предприятие, реализующая паттерн Result (монада Either).
Она решает главную проблему процедурного кода — неявную обработку ошибок и "пирамиды смерти" (глубокие вложенности Если...Тогда). Паттерн помогает писать надежный, читаемый код, в котором сложно "забыть" проверить ошибку, а бизнес-логика визуально отделена от обработки сбоев.
Обычно разработчики возвращают Неопределено или Ложь, а текст ошибки передают через параметр по ссылке: Функция СоздатьДокумент(Данные, ТекстОшибки = ""):
- Неявные контракты: По сигнатуре метода неочевидно, может ли он вернуть ошибку.
- Стрелочный код: Постоянные проверки
Если Данные = Неопределено Тогда...сдвигают полезный код глубоко вправо. - Сайд-эффекты: Изменение переменных по ссылке усложняет чтение кода.
- Риск
NullReference: Если забыть проверить результат, система упадет с ошибкой "Значение не является значением объектного типа".
Модуль инкапсулирует результат выполнения любой операции в единый стандартизированный объект. Функция всегда возвращает структуру, которая находится строго в одном из двух состояний:
- Успех (
Ок): содержит полезные данные. - Провал (
Ошибка): содержит понятное описание ошибки и технические детали.
- Создайте новый общий модуль с именем
Исход. - В свойствах модуля установите флаги:
- ✅ Сервер (здесь выполняется основная логика конвейеров).
- ✅ Клиент (управляемое приложение) (позволяет проверять статус на стороне фронтенда).
- ✅ Внешнее соединение (для интеграций).
- ❌ Вызов сервера (снимите флаг, архитектурно он здесь не нужен).
- Скопируйте содержимое файла
src/CommonModules/Исход/Module.bslв модуль. Готово!
Задача: Загрузить данные из внешнего API, создать документ Заказ, рассчитать по нему скидку и провести.
ТекстОшибки = "";
СырыеДанные = ПолучитьДанныеAPI(ИД, ТекстОшибки);
Если СырыеДанные <> Неопределено Тогда
ДокументСсылка = СоздатьЗаказ(СырыеДанные, ТекстОшибки);
Если ЗначениеЗаполнено(ДокументСсылка) Тогда
УспехСкидки = РассчитатьСкидки(ДокументСсылка, 10, ТекстОшибки);
Если УспехСкидки Тогда
ПровестиДокумент(ДокументСсылка, ТекстОшибки);
// ... и так далее
КонецЕсли;
КонецЕсли;
КонецЕсли;
Если ЗначениеЗаполнено(ТекстОшибки) Тогда
Сообщить(ТекстОшибки);
КонецЕсли;Код становится линейным конвейером. Если на шаге СоздатьЗаказ произойдет сбой, следующие шаги (РассчитатьСкидки) просто проигнорируются, а ошибка "пролетит" в самый конец:
// Передаем доп. параметр "10" (процент скидки) массивом или значением
Рез = Исход.Ок(ИД);
Рез = Исход.СвязатьДинамически(Рез, "Интеграция.ПолучитьДанныеAPI");
Рез = Исход.СвязатьДинамически(Рез, "ЗаказыКлиентов.СоздатьЗаказ");
Рез = Исход.СвязатьДинамически(Рез, "Скидки.РассчитатьСкидки", 10);
Рез = Исход.СвязатьДинамически(Рез, "ЗаказыКлиентов.ПровестиДокумент");
Если Исход.ЭтоОшибка(Рез) Тогда
Сообщить(Исход.ТекстОшибки(Рез));
Иначе
Сообщить("Заказ успешно проведен! Ссылка: " + Исход.Извлечь(Рез));
КонецЕсли;Вместо возврата произвольных значений, ваши функции бизнес-слоя теперь должны возвращать вызовы Исход.Ок() или Исход.Ошибка():
// Общий модуль: СкладскойУчет
Функция ЗарезервироватьТовар(Знач Товар, Знач Количество) Экспорт
Остаток = ПолучитьОстаток(Товар);
Если Остаток < Количество Тогда
// Возвращаем ошибку + доп. данные для логов
ДопДанные = Новый Структура("Товар, Остаток", Товар, Остаток);
Возврат Исход.Ошибка("Недостаточно товара на складе", ДопДанные);
КонецЕсли;
// Выполняем логику резервирования...
// Возвращаем успешный результат
Возврат Исход.Ок(Количество);
КонецФункцииИспользуется на самом верхнем уровне (например, в обработчике нажатия кнопки на форме):
ЭтоУспех(Рез)/ЭтоОшибка(Рез)ТекстОшибки(Рез)(Краткий текст для UI)ПодробныйТекстОшибки(Рез)(Детальный текст для логов)
Рез = СкладскойУчет.ЗарезервироватьТовар(Номенклатура, 5);
Если Исход.ЭтоОшибка(Рез) Тогда
// Для пользователя: "Недостаточно товара на складе"
Сообщить(Исход.ТекстОшибки(Рез));
// В журнал регистрации упадет и текст, и структура (Товар, Остаток)
ЗаписьЖурналаРегистрации("Склад", УровеньЖурналаРегистрации.Ошибка,,, Исход.ПодробныйТекстОшибки(Рез));
Возврат;
КонецЕсли;Как получить само значение из успешной монады:
ИзвлечьИли(Рез, ЗначениеПоУмолчанию): Безопасное извлечение.Извлечь(Рез): Жесткое извлечение (вызовет Исключение 1С при ошибке).РазвернутьИлиВызватьИсключение(Рез): Процедура для прерывания транзакции.
Рез = Ценообразование.ПолучитьЦену(Товар);
// Если цену получить не удалось, берем 0
Цена = Исход.ИзвлечьИли(Рез, 0);
// Если мы уверены, что цена ДОЛЖНА быть, иначе база неконсистентна:
ТочнаяЦена = Исход.Извлечь(Рез); // Упадет, если Рез = Ошибка
// Идеально перед Записать() внутри транзакции:
Исход.РазвернутьИлиВызватьИсключение(Рез);Это ядро библиотеки. Важно понимать разницу между методами:
СвязатьДинамически— вызывает функцию, которая САМА возвращаетИсход.ОкилиИсход.Ошибка.ОтобразитьДинамически— вызывает обычную функцию 1С (возвращающую строку, число и т.д.) и автоматически заворачивает её ответ вИсход.Ок.
(Примечание: Параметр ДопПараметры может быть массивом, если функция ожидает несколько аргументов).
ПараметрыСкидки = Новый Массив;
ПараметрыСкидки.Добавить(ТекущаяДатаСеанса()); // Параметр 2
ПараметрыСкидки.Добавить(Истина); // Параметр 3
Рез = Исход.Ок(ДокументСсылка); // В трубу залетает ссылка (Параметр 1)
// Функция сама вернет Исход:
Рез = Исход.СвязатьДинамически(Рез, "Скидки.ПрименитьСкидку", ПараметрыСкидки);
// Типовая функция возвращает просто Строку. Оборачиваем ее:
Рез = Исход.ОтобразитьДинамически(Рез, "ОбщегоНазначения.ПолучитьПредставлениеОбъекта");...Динамически используют платформенные вызовы Вычислить() и Выполнить(). В 1С такой код компилируется "на лету" (JIT-компиляция) при каждом вызове. Для разовых операций (проведение документа, вызов API) задержка микроскопична и незаметна.
Однако, не рекомендуется использовать методы Динамически в тяжелых циклах (на тысячи итераций) или в очень длинных цепочках вызовов, так как это замедлит выполнение. В таких высоконагруженных участках используйте паттерн раннего возврата в стиле Golang:
// Golang-style для максимальной производительности в циклах/длинных цепочках:
Рез = Интеграция.ПолучитьДанныеAPI(ИД);
Если Исход.ЭтоОшибка(Рез) Тогда
Возврат Рез; // Прерываем цепочку и отдаем ошибку наверх
КонецЕсли;
Рез = ЗаказыКлиентов.СоздатьЗаказ(Рез.Значение);
Если Исход.ЭтоОшибка(Рез) Тогда
Возврат Рез;
КонецЕсли;
// И так далее... Код работает максимально быстро без динамической компиляции.Либо:
Рез = ПолучитьДанныеAPI(Исход.Извлечь(Рез));
// Шаги (выполняются только если Рез - это успех)
Если Исход.Успех(Рез) Тогда
Рез = СоздатьЗаказ(Исход.Извлечь(Рез));
КонецЕсли;
Если Исход.Успех(Рез) Тогда
Рез = РассчитатьСкидки(Исход.Извлечь(Рез), 10);
КонецЕсли;
Если Исход.Успех(Рез) Тогда
Рез = ПровестиДокумент(Исход.Извлечь(Рез));
КонецЕсли;
// Финал: Обработка результата (Early return или ветвление)
Если Исход.Ошибка(Рез) Тогда
Возврат Рез; // Пробрасываем ошибку выше (как в Go!)
КонецЕсли;
Возврат Исход.Ок(Исход.Извлечь(Рез));Суффикс Попытаться... означает, что вызываемый метод будет обернут в Попытка... Исключение. Ошибки платформы (красные окна) превратятся в штатный Исход.Ошибка:
ПопытатьсяСвязатьДинамически(Рез, Метод)ПопытатьсяОтобразитьДинамически(Рез, Метод)
// Работа с нестабильным внешним кодом или файлами
Рез = Исход.Ок(ПутьКФайлу);
// Если файл заблокирован, 1С выкинет исключение. Мы его перехватим:
Рез = Исход.ПопытатьсяОтобразитьДинамически(Рез, "ФайловаяСистема.ПрочитатьТекст");
Если Исход.ЭтоОшибка(Рез) Тогда
// Мы спокойно обработали падение системы!
Сообщить("Сбой чтения файла: " + Исход.ТекстОшибки(Рез));
КонецЕсли;Используются для вызова ПРОЦЕДУР (которые ничего не возвращают) прямо в середине конвейера:
ПриУспехеДинамически(Рез, Процедура)ПриОшибкеДинамически(Рез, Процедура)
Рез = Исход.Ок(ЗаказСсылка);
Рез = Исход.СвязатьДинамически(Рез, "Интеграция.ОтправитьВCRM");
// Вызовется, только если отправка удалась
Рез = Исход.ПриУспехеДинамически(Рез, "Логирование.ЗаписатьУспешнуюОтправку");
// Вызовется, только если произошел сбой
Рез = Исход.ПриОшибкеДинамически(Рез, "Оповещения.ОтправитьПисьмоАдмину");СобратьМассив(МассивИсходов): Превращает массив результатов в один результат. Если упал хоть один — падает всё.ОбернутьОшибку(Рез, Префикс): Добавляет контекст к ошибке по мере её всплытия.
// 1. Обогащение контекста
Рез = Исход.Ошибка("Таймаут соединения");
Рез = Исход.ОбернутьОшибку(Рез, "Сбой загрузки прайса");
// Итог: "Сбой загрузки прайса: Таймаут соединения"
// 2. Сборка массива (обработка табличной части)
МассивРезультатов = Новый Массив;
Для Каждого Строка Из ТабличнаяЧасть Цикл
МассивРезультатов.Добавить( СкладскойУчет.ПроверитьСтроку(Строка) );
КонецЦикла;
// Если хоть одна строка с ошибкой, ИтоговыйРез будет содержать эту ошибку.
// Если все ок - ИтоговыйРез.Значение будет содержать массив результатов проверок.
ИтоговыйРез = Исход.СобратьМассив(МассивРезультатов);Версия(): Возвращает версию модуля (SemVer), полезно для проверки совместимости в сложных решениях.
- Используйте только API модуля. Строго запрещено напрямую обращаться к полям структуры (например, писать
Если Рез.Успех Тогда). ИспользуйтеЕсли Исход.ЭтоУспех(Рез) Тогда. Это защитит ваш код от любых будущих изменений под капотом библиотеки. - Исключения — для системы, Исход — для бизнеса. Если упал SQL-сервер — это системный сбой (оборачиваем через
ПопытатьсяСвязатьДинамически). Если у клиента не заполнен ИНН — это штатное бизнес-событие, возвращайтеИсход.Ошибка()напрямую, безВызватьИсключение. - Требования к функциям конвейера. Функции, вызываемые через динамические методы (
СвязатьДинамическии др.), должны быть Экспортными и располагаться на сервере в общих модулях или модулях менеджера. Для тяжелых вычислений отдавайте предпочтение прямым вызовам в стиле Golang (с проверкойЭтоОшибка()).