Искусственный сбой в коде 1С 8.3: проверяем, как программа переживает ошибки
Содержание
Вызвать ошибку в 1С проще всего оператором ВызватьИсключение "Текст ошибки"; — выполнение кода прерывается, и платформа ведёт себя так же, как при настоящем сбое. Для отладки и тестирования этого почти всегда достаточно: так проверяют, срабатывает ли Попытка…Исключение, откатывается ли транзакция и что видит пользователь.
Сложнее, когда нужен не «любой» сбой, а конкретный: конфликт блокировок, таймаут внешнего сервиса, падение на сервере, а не на клиенте. Для каждого такого случая есть свой приём, и весь код из примеров можно вставить во внешнюю обработку и запустить в тестовой базе.
Как платформа 1С обрабатывает исключения
Любая ошибка времени выполнения в 1С — это исключение. Деление на ноль, обращение к несуществующему полю, отказ записи объекта, ошибка запроса — всё это прерывает текущую процедуру и поднимается вверх по стеку вызовов.
Дальше два варианта:
- ✅ где-то выше есть блок
Попытка…Исключение— управление переходит в веткуИсключение, код продолжает работать; - ❌ такого блока нет — выполнение обрывается, пользователь видит окно с текстом ошибки, а платформа пишет событие в журнал регистрации.
Исключение пересекает границу клиента и сервера. Если оно возникло в серверной процедуре, вызванной из формы, клиент получит его с тем же текстом. Это удобно для тестов: достаточно бросить исключение в серверном коде и посмотреть, как отреагирует форма.
Отличайте исключение от отказа. В обработчиках ПередЗаписью, ОбработкаПроверкиЗаполнения, ОбработкаПроведения есть параметр Отказ. Установка Отказ = Истина тоже останавливает запись, но это штатный сценарий: объект не записан, исключения в вызывающем коде при интерактивной записи нет. Если же объект записывается программно через Записать(), отказ превратится в исключение — его и ловят в тестах.
Оператор ВызватьИсключение: синтаксис и примеры
Базовая форма — оператор с текстом:
Процедура ПроверитьСумму(Сумма) Экспорт
Если Сумма <= 0 Тогда
ВызватьИсключение СтрШаблон(НСтр("ru = 'Сумма должна быть больше нуля, получено: %1'"), Сумма);
КонецЕсли;
КонецПроцедуры
Текст лучше оборачивать в НСтр(): так сообщение будет переводиться в многоязычных конфигурациях. Для подстановки значений используйте СтрШаблон, а не склейку через + — числа без Формат() склеиваются с разделителями разрядов.
Повторный вызов внутри ветки «Исключение»
Оператор без параметров пишут только внутри блока Исключение. Он пробрасывает исходную ошибку дальше, не меняя её текст и место возникновения:
Попытка
ОбъектДокумента.Записать(РежимЗаписиДокумента.Проведение);
Исключение
// записали в журнал и отдали ошибку выше
ЗаписьЖурналаРегистрации("Обмен.Загрузка", УровеньЖурналаРегистрации.Ошибка,,,
ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
ВызватьИсключение;
КонецПопытки;
Это главный приём для отладки: вы фиксируете ошибку и при этом не «глотаете» её. Пустая ветка Исключение без записи в журнал — частая причина, по которой баг ищут неделями.
Расширенная форма в новых версиях платформы
Начиная с версии платформы 8.3.21 у оператора появилась форма с параметрами: кроме текста можно передать категорию ошибки, код, дополнительную информацию и причину. Код ошибки удобно проверять в автотестах вместо сравнения текстов. Точную сигнатуру смотрите в синтакс-помощнике своей версии: Справка → Синтакс-помощник, поиск по слову «ВызватьИсключение».
Быстрые способы сломать код без оператора
Иногда нужен «настоящий» платформенный сбой, а не свой текст:
| Что написать | Какая ошибка возникнет |
|---|---|
А = 1 / 0; | Деление на 0 |
Значение = Неопределено.Поле; | обращение к полю у значения неопределённого типа |
Новый COMОбъект("Нет.Такого.Объекта"); | ошибка создания COM-объекта (только Windows) |
Новый ЧтениеТекста("C:\нет\файла.txt"); | ошибка открытия файла |
Массив = Новый Массив; Х = Массив[5]; | индекс находится за границами диапазона |
Такие строки полезны, когда нужно проверить реакцию на ошибку именно платформы — например, что код логирования правильно разбирает её описание.
Перехват ошибок конструкцией Попытка…Исключение
Конструкция ловит любое исключение внутри блока Попытка, включая ошибки во вложенных вызовах:
Попытка
Результат = РассчитатьНаценку(Цена, Себестоимость);
Исключение
Результат = 0;
Сообщить(КраткоеПредставлениеОшибки(ИнформацияОбОшибке()));
КонецПопытки;
Три правила, которые избавляют от сюрпризов:
- 🎯 Оборачивайте минимальный участок кода. Блок на 200 строк скрывает, где именно упало.
- 🎯 Не ловите исключения «на всякий случай». Если вы не знаете, что делать с ошибкой, пусть она дойдёт до пользователя.
- 🎯 Внутри транзакции после пойманного исключения транзакцию нужно отменить, а не продолжать.
Транзакции: самая частая ловушка
Если ошибку поймать внутри транзакции и пойти дальше, следующая запись упадёт с текстом В данной транзакции уже происходили ошибки!. Транзакция после исключения помечена как сбойная, и зафиксировать её уже нельзя.
Правильный шаблон:
НачатьТранзакцию();
Попытка
ЗаписатьДанныеОбмена(Пакет);
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ВызватьИсключение;
КонецПопытки;
НачатьТранзакцию() стоит до Попытка: если ошибка возникнет в самом начале транзакции, ветка Исключение не попытается отменить то, чего нет.
Хороший тест для такого кода — вставить ВызватьИсключение "Тест отката"; сразу перед ЗафиксироватьТранзакцию() и проверить, что в базе не осталось частично записанных данных.
Разбор ошибки: ИнформацияОбОшибке и стек вызовов
Внутри ветки Исключение функция ИнформацияОбОшибке() возвращает объект с подробностями. Основные свойства:
| Свойство | Что содержит |
|---|---|
Описание | текст ошибки |
ИмяМодуля | модуль, где возникла ошибка |
НомерСтроки | номер строки в модуле |
ИсходнаяСтрока | текст строки кода |
Причина | вложенная ошибка (тоже ИнформацияОбОшибке) или Неопределено |
Ошибки бывают вложенными: например, запись документа упала, потому что упала подписка на событие. Цепочку причин разбирают циклом:
Инфо = ИнформацияОбОшибке();
Пока Инфо <> Неопределено Цикл
Сообщить(СтрШаблон("%1 (модуль %2, строка %3)",
Инфо.Описание, Инфо.ИмяМодуля, Формат(Инфо.НомерСтроки, "ЧГ=0")));
Инфо = Инфо.Причина;
КонецЦикла;
Краткое и подробное представление
Для готового текста есть две функции:
- 📝
КраткоеПредставлениеОшибки(Инфо)— только описание, подходит для сообщения пользователю; - 📝
ПодробноеПредставлениеОшибки(Инфо)— описание вместе с модулем, строкой и стеком, подходит для журнала.
В новых версиях платформы (8.3.17 и выше) рекомендован объект ОбработкаОшибок с аналогичными методами: ОбработкаОшибок.КраткоеПредставлениеОшибки() и ОбработкаОшибок.ПодробноеПредставлениеОшибки(). Через него же работают настройки отображения ошибок пользователю.
Стек вызовов в отладчике
Чтобы увидеть, какая цепочка процедур привела к ошибке, запустите конфигурацию в режиме отладки (F5 в Конфигураторе) и включите остановку по ошибке: Отладка → Остановка по ошибке. В этом окне отметьте флажок «Останавливаться по ошибке». Там же можно задать фильтр по тексту, чтобы отладчик реагировал только на нужную ошибку.
Когда отладчик остановится, откройте Отладка → Стек вызовов (Ctrl+Alt+C). Двойной щелчок по строке стека переводит к нужной процедуре, а в табло можно посмотреть значения переменных на каждом уровне.
Остановка по ошибке срабатывает и на исключениях, которые потом будут пойманы в Попытка. В типовых конфигурациях таких мест много, поэтому без фильтра отладчик будет останавливаться очень часто.
Для серверного кода в клиент-серверной базе отладка на сервере должна быть разрешена: служба сервера запускается с ключом -debug. Включение ключа требует перезапуска службы — все сеансы пользователей оборвутся, поэтому делайте это на тестовом сервере или вне рабочего времени.
Имитация сбоев базы данных и внешних сервисов
Свой текст в ВызватьИсключение проверяет логику обработки, но не поведение платформы. Для реальных сценариев сбой нужно воспроизвести честно.
ВызватьИсключение с понятным текстом в проверяемую процедуру. Этого достаточно.ЗафиксироватьТранзакцию() и проверьте, что данные не записались.Конфликт управляемых блокировок
Создайте внешнюю обработку с серверной процедурой, которая берёт исключительную блокировку и держит её заданное время:
&НаСервереБезКонтекста
Процедура ДержатьБлокировку(Секунд)
НачатьТранзакцию();
Попытка
Блокировка = Новый БлокировкаДанных;
Элемент = Блокировка.Добавить("РегистрНакопления.ТоварыНаСкладах"); // свой регистр
Элемент.Режим = РежимБлокировкиДанных.Исключительный;
Блокировка.Заблокировать();
Конец = ТекущаяУниверсальнаяДатаВМиллисекундах() + Секунд * 1000;
Пока ТекущаяУниверсальнаяДатаВМиллисекундах() < Конец Цикл
// ждём, удерживая блокировку
КонецЦикла;
ОтменитьТранзакцию();
Исключение
ОтменитьТранзакцию();
ВызватьИсключение;
КонецПопытки;
КонецПроцедуры
Запустите её в первом сеансе с параметром 60, а во втором проведите документ, который пишет в тот же регистр. Второй сеанс подождёт и получит ошибку Конфликт блокировок при выполнении транзакции: Превышено максимальное время ожидания предоставления блокировки. Так проверяют, что код обмена или регламентного задания корректно повторяет попытку.
Цикл ожидания загружает процессор, а блокировка остановит работу всех, кто пишет в этот регистр. Только тестовая база.
Недоступный HTTP-сервис
Для таймаута подключитесь к адресу из диапазона для документации 192.0.2.1 — там гарантированно никто не ответит:
Соединение = Новый HTTPСоединение("192.0.2.1", 80,,,, 5); // таймаут 5 секунд
Запрос = Новый HTTPЗапрос("/api/orders");
Попытка
Ответ = Соединение.Получить(Запрос);
Исключение
ЗаписьЖурналаРегистрации("Интеграция.Заказы", УровеньЖурналаРегистрации.Ошибка,,,
ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
КонецПопытки;
Здесь есть ловушка, которую многие пропускают: ответ с кодом 500 или 404 не вызывает исключения. Получить() отработает успешно, а ошибку нужно проверять по Ответ.КодСостояния. Поэтому тестируйте оба случая отдельно.
Чтобы получить управляемый ответ с ошибкой, опубликуйте в тестовой базе HTTP-сервис и в его обработчике верните Новый HTTPСервисОтвет(500). Так можно проверить и код 500, и неожиданное тело ответа, и долгую обработку.
Ошибки записи и проверки данных
Для сбоя при записи объекта есть простой вариант без правки типового кода: расширение конфигурации. Добавьте в расширение обработчик ПередЗаписью нужного документа, установите в нём Отказ = Истина и программно запишите документ. Получите честное исключение, а основную конфигурацию трогать не придётся. После теста расширение отключите.
Журнал регистрации и сообщения пользователю
Искусственная ошибка полезна, только если её последствия видно. Проверяйте две вещи: что записалось в журнал и что увидел пользователь.
Запись в журнал регистрации
ЗаписьЖурналаРегистрации(
"МояПодсистема.Загрузка", // имя события
УровеньЖурналаРегистрации.Ошибка, // уровень
Метаданные.Документы.РеализацияТоваровУслуг, // объект метаданных, можно пропустить
ДокументСсылка, // данные, можно пропустить
ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
Имя события задавайте с точкой и префиксом подсистемы — по нему потом фильтруют журнал. В БП 3.0 журнал открывается через Администрирование → Поддержка и обслуживание → Журнал регистрации, в Конфигураторе — Администрирование → Журнал регистрации.
Уровни выбирайте честно:
- 🔴
Ошибка— операция не выполнена; - 🟡
Предупреждение— выполнена, но с отклонениями; - 🔵
Информация— штатное событие, например начало обмена.
Сообщение пользователю
Пользователю показывайте краткое описание, без стека и номеров строк:
Сообщение = Новый СообщениеПользователю;
Сообщение.Текст = КраткоеПредставлениеОшибки(ИнформацияОбОшибке());
Сообщение.Поле = "Объект.Контрагент"; // подсветит поле формы
Сообщение.Сообщить();
Свойство Поле привязывает сообщение к реквизиту формы: при щелчке по сообщению курсор встанет в нужное поле. В конфигурациях на БСП то же делает ОбщегоНазначения.СообщитьПользователю().
На клиенте для показа полного окна ошибки есть ПоказатьИнформациюОбОшибке(ИнформацияОбОшибке()). Пользователь увидит то же окно, что и при необработанной ошибке, и сможет скопировать подробности для поддержки.
Автоматизация: ошибки в тестах
Ручная вставка ВызватьИсключение подходит для разовой проверки. Если сценарий нужно гонять после каждого изменения, оформите его тестом.
Принцип у всех фреймворков один: тест вызывает код и проверяет, что исключение возникло (или не возникло), а утверждение при несовпадении само бросает исключение. Минимальная проверка без фреймворка выглядит так:
Процедура Тест_ОтрицательнаяСуммаВызываетОшибку() Экспорт
ОшибкаБыла = Ложь;
Попытка
ПроверитьСумму(-100);
Исключение
ОшибкаБыла = СтрНайти(КраткоеПредставлениеОшибки(ИнформацияОбОшибке()), "больше нуля") > 0;
КонецПопытки;
Если Не ОшибкаБыла Тогда
ВызватьИсключение "Ожидалась ошибка для отрицательной суммы";
КонецЕсли;
КонецПроцедуры
Для системного подхода используют открытые фреймворки модульного и сценарного тестирования (например, Vanessa-ADD) и встроенное в платформу сценарное тестирование через клиент тестирования. Там же удобно хранить тесты на откат транзакций и таймауты.
Частые вопросы
Чем ВызватьИсключение отличается от Отказ = Истина?
Отказ штатно отменяет запись в обработчике события. ВызватьИсключение прерывает выполнение в любом месте кода и поднимает ошибку вверх по стеку.Можно ли вызвать ошибку в запросе?
Выполнить(). Для проверки логики удобнее свой текст через ВызватьИсключение.Почему ошибка пропала и ничего не случилось?
Попытка…Исключение с пустой веткой или без повторного вызова. Включите остановку по ошибке в отладчике и найдите место перехвата по стеку вызовов.Попадает ли пойманное исключение в журнал регистрации?
ЗаписьЖурналаРегистрации.Работает ли ВызватьИсключение в фоновом задании?
ИнформацияОбОшибке объекта фонового задания и в журнале регистрации.