Как ловить исключения через Попытку в 1С 8.3 и не ломать транзакции
Содержание
Попытка … Исключение … КонецПопытки — это конструкция встроенного языка 1С, которая перехватывает ошибку времени выполнения: если код внутри Попытка падает, управление переходит в блок Исключение, и программа не прерывается сообщением пользователю. С транзакциями она работает в паре по жёсткому правилу: НачатьТранзакцию() перед Попытка, ЗафиксироватьТранзакцию() последней строкой внутри, ОтменитьТранзакцию() первой строкой в Исключение.
Выбор обычно стоит между тремя вариантами. Не перехватывать ошибку вовсе — правильно для большей части кода. Перехватить и обработать — когда сбой ожидаем и понятно, что с ним делать: файл занят, внешний сервис не ответил, объект редактирует коллега. Перехватить, записать в журнал и пробросить дальше через ВызватьИсключение — стандартный ход для транзакций и серверной логики.
Синтаксис конструкции Попытка … Исключение
Минимальная форма выглядит так:
Попытка
// код, который может упасть
Результат = 100 / Делитель;
Исключение
// сюда попадаем только при ошибке
Сообщить("Не удалось посчитать: " + ОписаниеОшибки());
КонецПопытки;
Что важно знать про работу блока:
- 🔹 Если ошибки нет, блок
Исключениепропускается целиком. - 🔹 Если ошибка случилась в середине
Попытка, оставшиеся строки этого блока не выполняются. - 🔹 Ошибка ловится и тогда, когда она возникла глубоко в вызванной процедуре, на сервере или в обработчике события объекта.
- 🔹 Блока
Наконец(finally) в языке 1С нет. Код «выполнить в любом случае» пишут послеКонецПопыткиили дублируют в обеих ветках. - 🔹 Ошибки компиляции (синтаксические) конструкция не перехватывает — модуль с ними просто не запустится.
Английский вариант синтаксиса: Try … Except … EndTry. Ключевые слова можно писать в любом регистре, после КонецПопытки ставится точка с запятой.
Блок Исключение может быть пустым синтаксически, но пустым по смыслу он почти никогда быть не должен. Молча проглоченная ошибка — главный источник «загадочных» расхождений в данных.
Попытка и транзакции: правило размещения
Транзакция в 1С управляется тремя методами и одной функцией проверки:
| Метод | Что делает |
|---|---|
НачатьТранзакцию() | открывает транзакцию (или увеличивает счётчик вложенности) |
ЗафиксироватьТранзакцию() | фиксирует изменения в базе |
ОтменитьТранзакцию() | откатывает всё, что было сделано с начала транзакции |
ТранзакцияАктивна() | возвращает Истина, если транзакция открыта |
Каноническая схема, которую ждут на любом код-ревью:
НачатьТранзакцию();
Попытка
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("РегистрНакопления.ТоварыНаСкладах");
ЭлементБлокировки.УстановитьЗначение("Номенклатура", Номенклатура);
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
Блокировка.Заблокировать();
// чтение остатков, запись документов и регистров
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ЗаписьЖурналаРегистрации("Списание товаров",
УровеньЖурналаРегистрации.Ошибка, , ,
ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
ВызватьИсключение;
КонецПопытки;
Почему именно так:
НачатьТранзакцию()стоит доПопытка. Если он окажется внутри и упадёт, блокИсключениевызоветОтменитьТранзакцию()для транзакции, которой нет, и получит вторую ошибку «Транзакция не активна».ЗафиксироватьТранзакцию()— последняя строка вПопытка. Всё, что после фиксации, уже не откатится, поэтому там ничего быть не должно.ОтменитьТранзакцию()— первая строка вИсключение. Любое обращение к базе в «испорченной» транзакции до отката даст новую ошибку.ВызватьИсключениебез параметров в конце пробрасывает исходную ошибку наверх, чтобы вызывающий код узнал о сбое.
Почему нельзя продолжать работу после пойманной ошибки
Если внутри транзакции упал метод, работающий с базой (Записать(), Заблокировать(), выполнение запроса), платформа помечает транзакцию как сбойную. Перехватить такую ошибку и «пойти дальше» не получится. Следующая запись или фиксация вернёт:
Ошибка при вызове метода контекста (ЗафиксироватьТранзакцию): В данной транзакции уже происходили ошибки!
Транзакцию со сбоем можно только откатить целиком. Частичного отката до точки сохранения в 1С нет.
Вложенные транзакции
Настоящих вложенных транзакций платформа не поддерживает. Повторный НачатьТранзакцию() лишь увеличивает счётчик. Реальная фиксация происходит при последнем, самом внешнем ЗафиксироватьТранзакцию().
А вот ОтменитьТранзакцию() на любом уровне отменяет всё сразу. Поэтому внешний код, который «не заметил» отката во вложенной процедуре, упадёт на своей фиксации с той же ошибкой про уже происходившие ошибки. Отсюда правило: вложенная процедура после отката обязана пробросить исключение.
Метод Записать() у объекта сам открывает транзакцию, если её нет, и встраивается во внешнюю, если она есть. Обработчики ПередЗаписью, ОбработкаПроведения и подписки выполняются внутри неё.
Объект ИнформацияОбОшибке и функция ОписаниеОшибки
В блоке Исключение доступны два источника сведений о сбое.
ОписаниеОшибки() возвращает строку — текст ошибки, иногда с префиксом вроде {ОбщийМодуль.Склад.Модуль(42)}. Для сообщения пользователю этого обычно хватает.
ИнформацияОбОшибке() возвращает объект с полями:
| Свойство | Что содержит |
|---|---|
Описание | текст ошибки без служебных префиксов |
ИмяМодуля | модуль, где возникла ошибка |
НомерСтроки | номер строки в этом модуле |
ИсходнаяСтрока | текст строки кода, на которой упало |
Причина | вложенная ИнформацияОбОшибке или Неопределено |
Для журнала регистрации и для пользователя есть две готовые функции:
- 📝
ПодробноеПредставлениеОшибки(ИнформацияОбОшибке())— полный текст со стеком вызовов. Пишите его в журнал. - 💬
КраткоеПредставлениеОшибки(ИнформацияОбОшибке())— короткий текст без технических деталей. Показывайте его пользователю.
В актуальных версиях платформы те же функции вынесены в менеджер ОбработкаОшибок (ОбработкаОшибок.ПодробноеПредставлениеОшибки(...)), старые глобальные вызовы продолжают работать.
Разбор цепочки причин
Ошибка в обработчике записи доходит до вызывающего кода обёрнутой: сверху «Ошибка при вызове метода контекста (Записать)», а настоящая причина спрятана глубже. Пройти по цепочке можно циклом:
Инфо = ИнформацияОбОшибке();
Уровень = 0;
Пока Инфо <> Неопределено Цикл
Сообщить(СтрШаблон("%1. %2 (модуль %3, строка %4)",
Формат(Уровень, "ЧН=0; ЧГ=0"),
Инфо.Описание,
Инфо.ИмяМодуля,
Формат(Инфо.НомерСтроки, "ЧГ=0")));
Инфо = Инфо.Причина;
Уровень = Уровень + 1;
КонецЦикла;
Вызывайте ИнформацияОбОшибке() и ОписаниеОшибки() только внутри блока Исключение. Вне его результат не относится к вашей ошибке.
Вложенные Попытки и видимость переменных
Попытки можно вкладывать друг в друга на любую глубину. Ошибку ловит ближайший охватывающий блок:
Попытка
Для Каждого ИмяФайла Из СписокФайлов Цикл
Попытка
ЗагрузитьФайл(ИмяФайла);
Исключение
// ошибка одного файла: пишем в лог и идём к следующему
ЗаписьЖурналаРегистрации("Загрузка прайса",
УровеньЖурналаРегистрации.Предупреждение, , ИмяФайла,
ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
КонецПопытки;
КонецЦикла;
ОтправитьОтчетОЗагрузке();
Исключение
// сюда попадёт только ошибка ОтправитьОтчетОЗагрузке
// или ошибка, проброшенная из внутреннего блока
ВызватьИсключение;
КонецПопытки;
Что происходит при ошибке во внутреннем блоке:
- 🔸 Если внутренний
Исключениеошибку обработал и не пробросил — внешний блок о ней не узнает. - 🔸 Если внутренний блок вызвал
ВызватьИсключение— управление уходит во внешнийИсключение. - 🔸 Ошибка внутри самого блока
Исключениетоже уходит во внешний обработчик.
Области видимости в 1С нет на уровне блока
В отличие от C# или Java, переменная, впервые присвоенная внутри Попытка, видна во всей процедуре. Но если ошибка случилась раньше присваивания, в переменной останется Неопределено (или старое значение). Классическая ловушка:
Попытка
Соединение = Новый HTTPСоединение(Сервер, 443, , , , 30, Новый ЗащищенноеСоединениеOpenSSL);
Ответ = Соединение.Получить(Новый HTTPЗапрос("/api/rates"));
Исключение
ЗаписьЖурналаРегистрации("Курсы", УровеньЖурналаРегистрации.Ошибка, , ,
ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
КонецПопытки;
Если Ответ.КодСостояния = 200 Тогда // упадёт, если запрос не прошёл
Правильно — инициализировать переменную до блока (Ответ = Неопределено;) и проверять её после.
ВызватьИсключение: проброс и собственные ошибки
Оператор ВызватьИсключение (английский — Raise) работает в двух режимах:
| Форма | Где применять | Результат |
|---|---|---|
ВызватьИсключение; | только внутри Исключение | повторно выбрасывает исходную ошибку со всем стеком |
ВызватьИсключение "Текст"; | в любом месте | новая ошибка с вашим текстом |
Свою ошибку удобно выбрасывать при нарушении входных условий:
Если НЕ ЗначениеЗаполнено(Организация) Тогда
ВызватьИсключение НСтр("ru = 'Не указана организация для выгрузки'");
КонецЕсли;
В новых версиях платформы у ВызватьИсключение появился расширенный синтаксис со скобками: кроме текста можно передать категорию ошибки, код, дополнительные данные и причину. Если код должен работать и на старых платформах, пользуйтесь простой формой.
Если внутри Исключение вы выбрасываете новую ошибку с текстом, исходный стек для разработчика пропадает. Сначала запишите ПодробноеПредставлениеОшибки в журнал, потом выбрасывайте понятный пользователю текст.
Где Попытка действительно нужна
В типовом прикладном коде Попытка встречается реже, чем кажется новичкам. Её место — там, где сбой зависит от внешнего мира, а не от логики программы.
Внешние ресурсы. HTTP-запросы, COM-соединения (Новый COMОбъект("V83.COMConnector")), FTP, почта, работа с файлами на сетевом диске. Сеть может пропасть в любой момент — это ожидаемая ситуация.
Блокировка объекта для редактирования. Если документ открыт у коллеги, ЗаблокироватьДанныеДляРедактирования(Ссылка) выбросит исключение. Его ловят и показывают понятное сообщение:
Попытка
ЗаблокироватьДанныеДляРедактирования(ДокументСсылка);
Исключение
ВызватьИсключение СтрШаблон(НСтр("ru = 'Документ %1 сейчас редактирует другой пользователь'"),
ДокументСсылка);
КонецПопытки;
Пакетная обработка с транзакцией на каждый элемент. Перепроведение или загрузка сотен документов: сбой одного не должен откатывать остальные.
Для Каждого Ссылка Из СписокДокументов Цикл
НачатьТранзакцию();
Попытка
ДокументОбъект = Ссылка.ПолучитьОбъект();
ДокументОбъект.Записать(РежимЗаписиДокумента.Проведение);
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ЗаписьЖурналаРегистрации("Перепроведение",
УровеньЖурналаРегистрации.Ошибка, , Ссылка,
ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
КонецПопытки;
КонецЦикла;
Здесь ошибка сознательно не пробрасывается: обработка идёт дальше, а список сбойных документов потом смотрят в журнале регистрации по событию «Перепроведение».
Преобразование данных из внешних источников. Например, Число(СтрокаИзФайла) упадёт на пустой строке или на тексте с буквами. В загрузчиках такое преобразование заворачивают в Попытку и подставляют значение по умолчанию.
Регламентные и фоновые задания. Без перехвата упавшее задание оставит в журнале только общее сообщение. С Попыткой можно записать подробности и, если нужно, данные, на которых произошёл сбой.
Когда Попытка не нужна
- ❌ Для проверки, заполнено ли значение: есть
ЗначениеЗаполнено(). - ❌ Для проверки типа: есть
ТипЗнч(Значение) = Тип("Число"). - ❌ Для поиска в коллекции:
Найти()вернётНеопределено, исключения не будет. - ❌ Вокруг всего модуля «на всякий случай»: так прячутся настоящие ошибки.
Ошибки при работе с Попыткой и как их избежать
Ещё несколько практик, которые избавляют от проблем:
- ✅ Внутри
Исключениене делайте ничего, что само может упасть, кроме записи в журнал и проброса. - ✅ Не держите транзакцию открытой во время диалога с пользователем, HTTP-запроса или долгого расчёта: блокировки висят всё это время.
- ✅ Не заворачивайте в Попытку управляемую блокировку ради «повтора через секунду» внутри той же транзакции — после ошибки ожидания блокировки транзакция уже сбойная.
- ✅ На клиенте ловите ошибку серверного вызова, только если можете предложить пользователю действие. Иначе платформа сама покажет окно ошибки с подробностями.
- ✅ Текст для пользователя оборачивайте в
НСтр(), если конфигурация мультиязычная.