Как ловить исключения через Попытку в 1С 8.3 и не ломать транзакции

· 9 мин

Содержание

Попытка … Исключение … КонецПопытки — это конструкция встроенного языка 1С, которая перехватывает ошибку времени выполнения: если код внутри Попытка падает, управление переходит в блок Исключение, и программа не прерывается сообщением пользователю. С транзакциями она работает в паре по жёсткому правилу: НачатьТранзакцию() перед Попытка, ЗафиксироватьТранзакцию() последней строкой внутри, ОтменитьТранзакцию() первой строкой в Исключение.

Выбор обычно стоит между тремя вариантами. Не перехватывать ошибку вовсе — правильно для большей части кода. Перехватить и обработать — когда сбой ожидаем и понятно, что с ним делать: файл занят, внешний сервис не ответил, объект редактирует коллега. Перехватить, записать в журнал и пробросить дальше через ВызватьИсключение — стандартный ход для транзакций и серверной логики.

Синтаксис конструкции Попытка … Исключение

Минимальная форма выглядит так:

Попытка
    // код, который может упасть
    Результат = 100 / Делитель;
Исключение
    // сюда попадаем только при ошибке
    Сообщить("Не удалось посчитать: " + ОписаниеОшибки());
КонецПопытки;

Что важно знать про работу блока:

  • 🔹 Если ошибки нет, блок Исключение пропускается целиком.
  • 🔹 Если ошибка случилась в середине Попытка, оставшиеся строки этого блока не выполняются.
  • 🔹 Ошибка ловится и тогда, когда она возникла глубоко в вызванной процедуре, на сервере или в обработчике события объекта.
  • 🔹 Блока Наконец (finally) в языке 1С нет. Код «выполнить в любом случае» пишут после КонецПопытки или дублируют в обеих ветках.
  • 🔹 Ошибки компиляции (синтаксические) конструкция не перехватывает — модуль с ними просто не запустится.

Английский вариант синтаксиса: Try … Except … EndTry. Ключевые слова можно писать в любом регистре, после КонецПопытки ставится точка с запятой.

Блок Исключение может быть пустым синтаксически, но пустым по смыслу он почти никогда быть не должен. Молча проглоченная ошибка — главный источник «загадочных» расхождений в данных.

Попытка и транзакции: правило размещения

Транзакция в 1С управляется тремя методами и одной функцией проверки:

МетодЧто делает
НачатьТранзакцию()открывает транзакцию (или увеличивает счётчик вложенности)
ЗафиксироватьТранзакцию()фиксирует изменения в базе
ОтменитьТранзакцию()откатывает всё, что было сделано с начала транзакции
ТранзакцияАктивна()возвращает Истина, если транзакция открыта

Каноническая схема, которую ждут на любом код-ревью:

НачатьТранзакцию();
Попытка
    Блокировка = Новый БлокировкаДанных;
    ЭлементБлокировки = Блокировка.Добавить("РегистрНакопления.ТоварыНаСкладах");
    ЭлементБлокировки.УстановитьЗначение("Номенклатура", Номенклатура);
    ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
    Блокировка.Заблокировать();

    // чтение остатков, запись документов и регистров

    ЗафиксироватьТранзакцию();
Исключение
    ОтменитьТранзакцию();
    ЗаписьЖурналаРегистрации("Списание товаров",
        УровеньЖурналаРегистрации.Ошибка, , ,
        ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
    ВызватьИсключение;
КонецПопытки;

Почему именно так:

  1. НачатьТранзакцию() стоит до Попытка. Если он окажется внутри и упадёт, блок Исключение вызовет ОтменитьТранзакцию() для транзакции, которой нет, и получит вторую ошибку «Транзакция не активна».
  2. ЗафиксироватьТранзакцию() — последняя строка в Попытка. Всё, что после фиксации, уже не откатится, поэтому там ничего быть не должно.
  3. ОтменитьТранзакцию() — первая строка в Исключение. Любое обращение к базе в «испорченной» транзакции до отката даст новую ошибку.
  4. ВызватьИсключение без параметров в конце пробрасывает исходную ошибку наверх, чтобы вызывающий код узнал о сбое.

Почему нельзя продолжать работу после пойманной ошибки

Если внутри транзакции упал метод, работающий с базой (Записать(), Заблокировать(), выполнение запроса), платформа помечает транзакцию как сбойную. Перехватить такую ошибку и «пойти дальше» не получится. Следующая запись или фиксация вернёт:

Ошибка при вызове метода контекста (ЗафиксироватьТранзакцию): В данной транзакции уже происходили ошибки!

Транзакцию со сбоем можно только откатить целиком. Частичного отката до точки сохранения в 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 сейчас редактирует другой пользователь'"),
        ДокументСсылка);
КонецПопытки;

Пакетная обработка с транзакцией на каждый элемент. Перепроведение или загрузка сотен документов: сбой одного не должен откатывать остальные.

Для Каждого Ссылка Из СписокДокументов Цикл
    НачатьТранзакцию();
    Попытка
        ДокументОбъект = Ссылка.ПолучитьОбъект();
        ДокументОбъект.Записать(РежимЗаписиДокумента.Проведение);
        ЗафиксироватьТранзакцию();
    Исключение
        ОтменитьТранзакцию();
        ЗаписьЖурналаРегистрации("Перепроведение",
            УровеньЖурналаРегистрации.Ошибка, , Ссылка,
            ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
    КонецПопытки;
КонецЦикла;

Здесь ошибка сознательно не пробрасывается: обработка идёт дальше, а список сбойных документов потом смотрят в журнале регистрации по событию «Перепроведение».

Преобразование данных из внешних источников. Например, Число(СтрокаИзФайла) упадёт на пустой строке или на тексте с буквами. В загрузчиках такое преобразование заворачивают в Попытку и подставляют значение по умолчанию.

Регламентные и фоновые задания. Без перехвата упавшее задание оставит в журнале только общее сообщение. С Попыткой можно записать подробности и, если нужно, данные, на которых произошёл сбой.

Когда Попытка не нужна

  • ❌ Для проверки, заполнено ли значение: есть ЗначениеЗаполнено().
  • ❌ Для проверки типа: есть ТипЗнч(Значение) = Тип("Число").
  • ❌ Для поиска в коллекции: Найти() вернёт Неопределено, исключения не будет.
  • ❌ Вокруг всего модуля «на всякий случай»: так прячутся настоящие ошибки.

Ошибки при работе с Попыткой и как их избежать

Диагностика: выберите симптом

Ещё несколько практик, которые избавляют от проблем:

  1. ✅ Внутри Исключение не делайте ничего, что само может упасть, кроме записи в журнал и проброса.
  2. ✅ Не держите транзакцию открытой во время диалога с пользователем, HTTP-запроса или долгого расчёта: блокировки висят всё это время.
  3. ✅ Не заворачивайте в Попытку управляемую блокировку ради «повтора через секунду» внутри той же транзакции — после ошибки ожидания блокировки транзакция уже сбойная.
  4. ✅ На клиенте ловите ошибку серверного вызова, только если можете предложить пользователю действие. Иначе платформа сама покажет окно ошибки с подробностями.
  5. ✅ Текст для пользователя оборачивайте в НСтр(), если конфигурация мультиязычная.

Частые вопросы

Сильно ли Попытка замедляет код?
Сам вход в блок почти ничего не стоит. Дорого обходится выброс исключения: формирование информации об ошибке и стека. Поэтому Попытку не используют как замену обычной проверке условия в цикле на тысячи строк.
Поймает ли клиентская Попытка ошибку, которая случилась на сервере?
Да. Исключение из серверной процедуры передаётся на клиент вместе с информацией об ошибке, и клиентский блок Исключение его перехватит. Транзакцию при этом нужно было откатить ещё на сервере: на клиенте транзакций нет.
Чем ОписаниеОшибки() отличается от ИнформацияОбОшибке().Описание?
Функция ОписаниеОшибки() возвращает текст вместе с префиксом модуля и строки, а свойство Описание — только сам текст ошибки. Для пользователя удобнее КраткоеПредставлениеОшибки().
Откатятся ли записи в журнал регистрации вместе с транзакцией?
Нет. ЗаписьЖурналаРегистрации пишет вне транзакции базы, поэтому запись сохранится даже после ОтменитьТранзакцию(). На этом и строится логирование в блоке Исключение.
Нужно ли проверять ТранзакцияАктивна() перед откатом?
При правильной схеме, когда НачатьТранзакцию() стоит перед Попыткой, транзакция в блоке Исключение открыта. Проверку добавляют в обобщённых процедурах, которые вызываются и в транзакции, и вне её.