Разработка механизмов пакетного создания документов или сложных цепочек проведения в 1С неизбежно сталкивает программиста с необходимостью использования транзакций. Типичная ситуация: мы оборачиваем код в НачатьТранзакцию() и Попытка...Исключение, но при возникновении сбоя блок Исключение не дает нам никакой информации о причинах — функция ОписаниеОшибки() возвращает пустую строку. В этой статье мы подробно разберем, почему так происходит, как работает механизм исключений в 1С и как гарантированно получить текст ошибки для пользователя.
Первое, что нам необходимо понять: в платформе 1С существует два принципиально разных способа сигнализировать об ошибке. Проанализируем их:
ВызватьИсключение. Только их перехватывает конструкция Попытка...Исключение.Отказ в значение Истина и выводят текст сообщения пользователю через метод ОбщегоНазначения.СообщитьПользователю.Если во вложенной процедуре (например, при проведении типового документа) произошел логический отказ, выполнение кода продолжается, но метод Записать() возвращает Ложь. Поскольку исключения не было, блок Исключение не срабатывает. Если же вы сами вызвали Исключение, но перед этим транзакция уже была «испорчена» внутренней ошибкой платформы, вы можете столкнуться с тем, что текст ошибки будет неинформативным.
Рассмотрим важный нюанс работы СУБД через платформу 1С. Если внутри транзакции произошло реальное исключение (например, нарушение уникальности индексов или ошибка блокировки), транзакция переходит в состояние «битой».
В этом состоянии любые попытки записи в базу данных (даже других объектов) будут вызывать новую системную ошибку: «В данной транзакции уже происходили ошибки!». Это защитный механизм платформы, который запрещает частичную фиксацию данных. Пока вы не вызовете ОтменитьТранзакцию(), работа с базой будет заблокирована. Именно поэтому критически важно правильно располагать вызовы НачатьТранзакцию() и Попытка.
Выясним, как правильно выстроить структуру кода. Распространенная ошибка — помещать НачатьТранзакцию() внутрь блока Попытка. Правильный паттерн выглядит так:
НачатьТранзакцию();
Попытка
// Выполняем действия с документами
Если Не ВыполнитьДействие() Тогда
// Если это логический отказ, а не исключение
ВызватьИсключение "Не удалось выполнить операцию. См. сообщения ниже.";
КонецЕсли;
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
// Здесь мы обрабатываем ошибку
КонецПопытки;
Важно: Вызов ОтменитьТранзакцию() должен быть первым действием в блоке Исключение, чтобы снять блокировку и иметь возможность записывать логи или выводить сообщения.
Если ОписаниеОшибки() пустое, значит, код, который вызвал сбой, просто вывел сообщение в стек пользователя. Рассмотрим, как их извлечь. Метод ПолучитьСообщенияПользователю() возвращает массив объектов, которые были накоплены системой в процессе выполнения кода.
Посмотрим на пример обработки сообщений в блоке исключения:
Исключение
ОтменитьТранзакцию();
// Собираем все сообщения, которые накопились в стеке
МассивСообщений = ПолучитьСообщенияПользователю(Истина);
Для Каждого ТекстСообщения Из МассивСообщений Цикл
ОбщегоНазначения.СообщитьПользователю(ТекстСообщения.Текст);
КонецЦикла;
// Дополнительно получаем системную информацию
ИнфоОбОшибке = ИнформацияОбОшибке();
Подробности = ПодробноеПредставлениеОшибки(ИнфоОбОшибке);
ЗаписьЖурналаРегистрации("Событие.Ошибка", УровеньЖурналаРегистрации.Ошибка, , , Подробности);
КонецПопытки;
Проанализируем ситуацию: почему опытные разработчики не рекомендуют использовать ОписаниеОшибки()? Эта функция возвращает только краткий текст, чего часто недостаточно для диагностики в сложных модулях.
Вместо неё мы используем объект ИнформацияОбОшибке. Он позволяет получить:
КраткоеПредставлениеОшибки — суть проблемы.ПодробноеПредставлениеОшибки — самое важное: включает стек вызовов. Вы увидите не просто «Ошибка при записи», а конкретный модуль и номер строки, где это произошло.Причина — позволяет докопаться до исходного исключения в цепочке вложенных вызовов.Соберем всё вышесказанное в единый надежный алгоритм для создания и проведения документов в транзакции:
НачатьТранзакцию();
Попытка
// Создаем объект
НовыйДок = Документы.РеализацияТоваровУслуг.СоздатьДокумент();
// ... заполнение реквизитов ...
// Попытка записи с проверкой результата
Если Не НовыйДок.Записать(РежимЗаписиДокумента.Проведение) Тогда
// Если проведение не удалось, исключение не вылетает само,
// но транзакцию фиксировать нельзя!
ВызватьИсключение "Ошибка проведения документа " + НовыйДок.Ссылка;
КонецЕсли;
ЗафиксироватьТранзакцию();
Исключение
// 1. Сразу откатываем транзакцию
Если ТранзакцияАктивна() Тогда
ОтменитьТранзакцию();
КонецЕсли;
// 2. Получаем системную ошибку
Инфо = ИнформацияОбОшибке();
ТекстОшибки = КраткоеПредставлениеОшибки(Инфо);
// 3. Собираем сообщения (те самые "Период закрыт" или "Недостаточно товара")
МассивСообщений = ПолучитьСообщенияПользователю(Истина);
Для Каждого Сообщение Из МассивСообщений Цикл
// Выводим их пользователю или склеиваем в текст лога
ОбщегоНазначения.СообщитьПользователю(Сообщение.Текст);
КонецЦикла;
// 4. Логируем подробности для разработчика
ЗаписьЖурналаРегистрации("ОбработкаДокументов", УровеньЖурналаРегистрации.Ошибка, , , ПодробноеПредставлениеОшибки(Инфо));
КонецПопытки;
Таким образом, комбинирование проверки результата метода Записать(), извлечение стека сообщений через ПолучитьСообщенияПользователю() и анализ ИнформацияОбОшибке() гарантирует, что ни одна ошибка не останется незамеченной, а пользователь всегда получит понятный ответ о причинах сбоя.