Skill v1.0.1
Automated scan100/100+1 new
version: "1.0.1" name: error-handling description: "MUST use WHEN обрабатываешь исключения или управляешь транзакциями в BSL. Provides каноническую схему Попытка/Исключение, правила отката транзакций и управления блокировками данных." alwaysApply: false
Обработка ошибок, транзакции и блокировки
Ключевой принцип: В 1С нет автоматического управления транзакциями. Разработчик вручную управляет началом, фиксацией и откатом. Каждая незакрытая транзакция — потенциальная катастрофа.
Правило 1: Попытка/Исключение — всегда логируйте, никогда не глотайте
Проглоченное исключение — самый опасный антипаттерн: данные не записаны, пользователь не знает об ошибке, в ЖР нет следов, отладка невозможна.
Стандарт ИТС: «В обработчике исключения обязательно фиксировать информацию об ошибке в журнале регистрации».
Канонический паттерн обработки исключений
ПопыткаДокументОбъект.Записать(РежимЗаписиДокумента.Проведение);ИсключениеИнформацияОбОшибке = ИнформацияОбОшибке();ЗаписьЖурналаРегистрации(НСтр("ru = 'Проведение документа'"),УровеньЖурналаРегистрации.Ошибка,ДокументОбъект.Метаданные(),ДокументОбъект.Ссылка,ПодробноеПредставлениеОшибки(ИнформацияОбОшибке));ВызватьИсключение;КонецПопытки;
Два представления ошибки
| Функция | Когда использовать | Что содержит | |
|---|---|---|---|
КраткоеПредставлениеОшибки() | Для отображения пользователю | Понятный текст без технических деталей | |
ПодробноеПредставлениеОшибки() | Для журнала регистрации | Полный стек вызовов, номера строк, вложенные ошибки |
Пример: разные уровни обработки
// Нижний уровень — логирование + пробросФункция ЗаписатьДокумент(ДокументОбъект)ПопыткаДокументОбъект.Записать(РежимЗаписиДокумента.Проведение);Возврат Истина;ИсключениеЗаписьЖурналаРегистрации(НСтр("ru = 'Проведение документа'"),УровеньЖурналаРегистрации.Ошибка,ДокументОбъект.Метаданные(),ДокументОбъект.Ссылка,ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));ВызватьИсключение;КонецПопытки;КонецФункции// Верхний уровень (форма) — показ пользователю&НаКлиентеПроцедура ЗаписатьДокумент(Команда)ПопыткаЗаписатьДокументНаСервере();ИсключениеПоказатьПредупреждение(,НСтр("ru = 'Не удалось записать документ. Обратитесь к администратору.'"));КонецПопытки;КонецПроцедуры
Правило 2: Канонический паттерн транзакций
Незакрытая транзакция блокирует записи в СУБД. Другие сеансы ждут (таймаут ~20 сек) и получают ошибку.
Стандарт ИТС: «Транзакции: правила использования» — НачатьТранзакцию() ВСЕГДА непосредственно перед Попытка.
Канонический паттерн (ОБЯЗАТЕЛЬНЫЙ)
НачатьТранзакцию();Попытка// 1. Блокировка данных (если нужно — см. правило 5)Блокировка = Новый БлокировкаДанных;ЭлементБлокировки = Блокировка.Добавить("Документ.РеализацияТоваровУслуг");ЭлементБлокировки.УстановитьЗначение("Ссылка", ДокументСсылка);Блокировка.Заблокировать();// 2. Чтение и модификация данныхДокументОбъект = ДокументСсылка.ПолучитьОбъект();ДокументОбъект.Статус = Перечисления.СтатусыДокументов.Согласован;// 3. ЗаписьДокументОбъект.Записать();// === Фиксация — ПОСЛЕДНЯЯ операция перед Исключение ===ЗафиксироватьТранзакцию();Исключение// === Откат — ПЕРВАЯ операция в блоке Исключение ===ОтменитьТранзакцию();// Логирование ПОСЛЕ отката (запись в ЖР внутри отменённой транзакции будет потеряна!)ЗаписьЖурналаРегистрации(НСтр("ru = 'Согласование документа'"),УровеньЖурналаРегистрации.Ошибка,Метаданные.Документы.РеализацияТоваровУслуг,ДокументСсылка,ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));ВызватьИсключение;КонецПопытки;
Критические правила порядка
| Требование | Почему | |
|---|---|---|
НачатьТранзакцию() непосредственно перед Попытка | Если между ними ошибка — транзакция не закроется | |
ЗафиксироватьТранзакцию() — последний оператор перед Исключение | Операция после фиксации при ошибке не откатится | |
ОтменитьТранзакцию() — первый оператор в Исключение | Логирование тоже может вызвать ошибку; если откат ещё не сделан — каскадная проблема | |
ЗаписьЖурналаРегистрации() — ПОСЛЕ ОтменитьТранзакцию() | Запись в ЖР внутри отменённой транзакции будет потеряна |
Неправильные варианты (ловушки платформы)
// ПЛОХО: код между НачатьТранзакцию и ПопыткаНачатьТранзакцию();ПодготовитьДанные(); // Если здесь ошибка — транзакция зависнет!Попытка// ...КонецПопытки;// ПЛОХО: ЗаписьЖурнала ДО ОтменитьТранзакциюИсключениеЗаписьЖурналаРегистрации(...); // Может быть потеряна при откате!ОтменитьТранзакцию();КонецПопытки;// ПЛОХО: код после ЗафиксироватьТранзакцию, но до конца ПопыткаЗафиксироватьТранзакцию();ОтправитьОповещение(); // Ошибка здесь — транзакция уже зафиксирована, но Исключение выполнится!ИсключениеОтменитьТранзакцию(); // Ошибка! Транзакция уже зафиксирована!КонецПопытки;
Правило 3: Вложенные транзакции — счётчиковые
В 1С вложенный НачатьТранзакцию() не создаёт новую транзакцию, а увеличивает счётчик. ОтменитьТранзакцию() помечает транзакцию как «отменённая», и любой последующий ЗафиксироватьТранзакцию() (даже на внешнем уровне) вызовет исключение.
Правильно — каждый уровень использует канонический паттерн и пробрасывает исключение
Процедура ЗаписатьДанные(ДанныеДляЗаписи)НачатьТранзакцию();ПопыткаДокументОбъект.Записать();ЗафиксироватьТранзакцию();ИсключениеОтменитьТранзакцию();ЗаписьЖурналаРегистрации(НСтр("ru = 'Запись данных'"),УровеньЖурналаРегистрации.Ошибка,,,ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));ВызватьИсключение; // ОБЯЗАТЕЛЬНО пробрасываем — внешний код должен знатьКонецПопытки;КонецПроцедуры
Правило: НЕ используйте ТранзакцияАктивна() как замену правильному паттерну
// ПЛОХО: ТранзакцияАктивна() маскирует ошибку в структуре кодаПопыткаНачатьТранзакцию();// ...ЗафиксироватьТранзакцию();ИсключениеЕсли ТранзакцияАктивна() ТогдаОтменитьТранзакцию();КонецЕсли;КонецПопытки;// ПРАВИЛЬНО: корректная структура делает проверку ненужнойНачатьТранзакцию();Попытка// ...ЗафиксироватьТранзакцию();ИсключениеОтменитьТранзакцию();ВызватьИсключение;КонецПопытки;
Правило 4: Минимизация времени транзакции
Пока транзакция открыта — изменённые данные заблокированы в СУБД. Длинная транзакция = каскадные блокировки = пользователи не могут работать.
// Подготовка данных — ВНЕ транзакцииМассивДанных = ПодготовитьДанные();ПроверитьКорректность(МассивДанных);// Транзакция — только быстрые операции записиНачатьТранзакцию();ПопыткаДля Каждого ДанныеСтроки Из МассивДанных ЦиклЗаписатьСтроку(ДанныеСтроки);КонецЦикла;ЗафиксироватьТранзакцию();ИсключениеОтменитьТранзакцию();ЗаписьЖурналаРегистрации(...);ВызватьИсключение;КонецПопытки;
Внешние HTTP-вызовы внутри транзакции — катастрофа: таймаут HTTP = 30 сек = блокировка 30 сек.
Правило 5: Управляемые блокировки — БлокировкаДанных
Без гранулярной блокировки перед чтением-модификацией возникает race condition: два сеанса читают одно значение, оба модифицируют — одно обновление потеряно.
Стандарт ИТС: «Управляемые блокировки».
Паттерн: заблокировать-прочитать-изменить-записать
НачатьТранзакцию();Попытка// 1. СНАЧАЛА блокируемБлокировка = Новый БлокировкаДанных;ЭлементБлокировки = Блокировка.Добавить("РегистрНакопления.ТоварыНаСкладах");ЭлементБлокировки.УстановитьЗначение("Номенклатура", НоменклатураСсылка);ЭлементБлокировки.УстановитьЗначение("Склад", СкладСсылка);ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;Блокировка.Заблокировать();// 2. Читаем — гарантированно актуальные данныеЗапрос = Новый Запрос;Запрос.Текст ="ВЫБРАТЬ| Остатки.КоличествоОстаток КАК Остаток|ИЗ| РегистрНакопления.ТоварыНаСкладах.Остатки(,| Номенклатура = &Номенклатура И Склад = &Склад) КАК Остатки";Запрос.УстановитьПараметр("Номенклатура", НоменклатураСсылка);Запрос.УстановитьПараметр("Склад", СкладСсылка);Результат = Запрос.Выполнить();Если Результат.Пустой() ТогдаВызватьИсключение НСтр("ru = 'Нет остатков на складе.'");КонецЕсли;Выборка = Результат.Выбрать();Выборка.Следующий();// 3. ПроверяемЕсли Выборка.Остаток < ТребуемоеКоличество ТогдаВызватьИсключение СтрШаблон(НСтр("ru = 'Недостаточно остатков. На складе: %1, требуется: %2.'"),Выборка.Остаток, ТребуемоеКоличество);КонецЕсли;// 4. Записываем// ... запись движений ...ЗафиксироватьТранзакцию();ИсключениеОтменитьТранзакцию();ЗаписьЖурналаРегистрации(...);ВызватьИсключение;КонецПопытки;
Почему блокировка ПЕРЕД чтением
Без блокировки (race condition):Сеанс A: Читает остаток = 10 | Сеанс B: Читает остаток = 10Сеанс A: 10 >= 8? Да, списываем 8 | Сеанс B: 10 >= 7? Да, списываем 7Итого: списано 15 единиц при остатке 10 → отрицательный остаток!С блокировкой:Сеанс A: Блокирует → Читает 10 → Списывает 8 → Фиксирует → РазблокируетСеанс B: Ждёт блокировку → Читает 2 → 2 < 7 → Ошибка (корректная!)
Правило 6: ЗаблокироватьДанныеДляРедактирования — пессимистичная блокировка объектов
Предотвращает lost update: второй пользователь получит ошибку «Объект заблокирован пользователем X».
Процедура ИзменитьСтатусДокумента(ДокументСсылка, НовыйСтатус)НачатьТранзакцию();ПопыткаЗаблокироватьДанныеДляРедактирования(ДокументСсылка);ДокументОбъект = ДокументСсылка.ПолучитьОбъект();ДокументОбъект.Статус = НовыйСтатус;ДокументОбъект.Записать();ЗафиксироватьТранзакцию();ИсключениеОтменитьТранзакцию();ЗаписьЖурналаРегистрации(НСтр("ru = 'Изменение статуса документа'"),УровеньЖурналаРегистрации.Ошибка,,,ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));ВызватьИсключение;КонецПопытки;КонецПроцедуры
Разница между блокировками
| Тип блокировки | Механизм | Когда использовать | |
|---|---|---|---|
БлокировкаДанных | СУБД (управляемая), уровень записей | Контроль остатков, атомарные операции | |
ЗаблокироватьДанныеДляРедактирования | Сервер 1С (пессимистичная), объект целиком | Предотвращение lost update |
Правило 7: ЗаписьЖурналаРегистрации — правильное логирование
Полный формат записи
ЗаписьЖурналаРегистрации(ИмяСобытия, // Строка — иерархическое имя (через точку)УровеньСобытия, // УровеньЖурналаРегистрации — Ошибка/Предупреждение/Информация/ПримечаниеМетаданныеОбъекта, // Объект метаданных — для фильтрации по типуДанные, // Ссылка на объект — для навигации из ЖРКомментарий); // Строка — подробное описание (до 1024 символов)
Уровни
| Уровень | Когда | |
|---|---|---|
Ошибка | Операция не выполнена, данные потеряны или некорректны | |
Предупреждение | Операция выполнена, но с ограничениями | |
Информация | Значимые события для аудита | |
Примечание | Диагностическая информация |
Пример: структурированное логирование
ИмяСобытия = НСтр("ru = 'ОбменДанными.ОтправкаДанных.Ошибка'");Комментарий = СтрШаблон(НСтр("ru = 'Ошибка при отправке данных в узел ""%1"".|Количество объектов: %2.|Текст ошибки:|%3'"),Строка(УзелОбмена),КоличествоОбъектов,ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));ЗаписьЖурналаРегистрации(ИмяСобытия,УровеньЖурналаРегистрации.Ошибка,Метаданные.ПланыОбмена.ОбменСКонтрагентами,УзелОбмена,Комментарий);
Правило 8: Формирование сообщений об ошибках для пользователя
Пользователю — что произошло и что делать. В ЖР — техническую информацию.
ПопыткаДокументОбъект.Записать(РежимЗаписиДокумента.Проведение);ИсключениеЗаписьЖурналаРегистрации(НСтр("ru = 'Проведение документа'"),УровеньЖурналаРегистрации.Ошибка,ДокументОбъект.Метаданные(),ДокументОбъект.Ссылка,ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));ТекстДляПользователя = СтрШаблон(НСтр("ru = 'Не удалось провести документ ""%1"".|Попробуйте повторить операцию. Если ошибка повторяется, обратитесь к администратору.||Техническая информация: %2'"),ДокументОбъект,КраткоеПредставлениеОшибки(ИнформацияОбОшибке()));ВызватьИсключение ТекстДляПользователя;КонецПопытки;
Правило 9: Правильный проброс исключений — ВызватьИсключение
| Способ | Когда | Почему | |
|---|---|---|---|
ВызватьИсключение; | В промежуточном коде (модуль объекта, общий модуль) | Сохраняет оригинальный стек | |
ВызватьИсключение "Текст"; | На границе с пользователем (форма) | Заменяет технический стек на понятное сообщение |
// Промежуточный слой — пробрасываем оригиналПроцедура ОбработатьДанные(Данные)ПопыткаЗаписатьДанные(Данные);ИсключениеЗаписьЖурналаРегистрации(...);ВызватьИсключение; // Оригинальный стек сохранёнКонецПопытки;КонецПроцедуры// Граница с пользователем&НаСервереПроцедура ОбработатьНаСервере()ПопыткаОбработатьДанные(ДанныеФормы);ИсключениеВызватьИсключение СтрШаблон(НСтр("ru = 'Ошибка обработки данных: %1'"),КраткоеПредставлениеОшибки(ИнформацияОбОшибке()));КонецПопытки;КонецПроцедуры
Правило 10: Обработка ошибок при массовых операциях
Ошибка в одном документе не должна останавливать обработку остальных. Каждая транзакция — поэлементная.
Процедура ПровестиДокументыПакетно(МассивДокументов)МассивОшибок = Новый Массив;Для Каждого ДокументСсылка Из МассивДокументов ЦиклНачатьТранзакцию();ПопыткаДокументОбъект = ДокументСсылка.ПолучитьОбъект();ДокументОбъект.Записать(РежимЗаписиДокумента.Проведение);ЗафиксироватьТранзакцию();ИсключениеОтменитьТранзакцию();ИнфоОшибки = ИнформацияОбОшибке();ЗаписьЖурналаРегистрации(НСтр("ru = 'Пакетное проведение'"),УровеньЖурналаРегистрации.Ошибка,ДокументСсылка.Метаданные(),ДокументСсылка,ПодробноеПредставлениеОшибки(ИнфоОшибки));МассивОшибок.Добавить(Новый Структура("Документ, Ошибка",ДокументСсылка,КраткоеПредставлениеОшибки(ИнфоОшибки)));КонецПопытки;КонецЦикла;Если МассивОшибок.Количество() > 0 ТогдаДля Каждого ОписаниеОшибки Из МассивОшибок ЦиклОбщегоНазначения.СообщитьПользователю(СтрШаблон(НСтр("ru = 'Документ %1: %2'"),ОписаниеОшибки.Документ, ОписаниеОшибки.Ошибка));КонецЦикла;КонецЕсли;КонецПроцедуры
Правило 11: Единый порядок блокировки — предотвращение deadlock
Когда два сеанса блокируют данные в разном порядке — deadlock. СУБД откатывает одну из транзакций.
// Всегда блокируйте ресурсы в фиксированном порядке (по ссылке)МассивСсылок = ОбщегоНазначенияКлиентСервер.СвернутьМассив(МассивДокументов);МассивСсылок.СортироватьПоЗначению();Для Каждого Ссылка Из МассивСсылок ЦиклЗаблокироватьДанныеДляРедактирования(Ссылка);КонецЦикла;
Обработка ошибки блокировки
НачатьТранзакцию();ПопыткаБлокировка = Новый БлокировкаДанных;ЭлементБлокировки = Блокировка.Добавить("Справочник.Номенклатура");ЭлементБлокировки.УстановитьЗначение("Ссылка", НоменклатураСсылка);ПопыткаБлокировка.Заблокировать();ИсключениеОтменитьТранзакцию();ВызватьИсключение СтрШаблон(НСтр("ru = 'Не удалось заблокировать ""%1"". Данные редактируются другим пользователем.'"),НоменклатураСсылка);КонецПопытки;НоменклатураОбъект = НоменклатураСсылка.ПолучитьОбъект();НоменклатураОбъект.Записать();ЗафиксироватьТранзакцию();ИсключениеЕсли ТранзакцияАктивна() ТогдаОтменитьТранзакцию();КонецЕсли;ЗаписьЖурналаРегистрации(...);ВызватьИсключение;КонецПопытки;
Правило 12: Попытка/Исключение для внешних вызовов
Вызовы внешних систем ненадёжны. Всегда оборачивайте в Попытка/Исключение.
Паттерн: HTTP-вызов с повторными попытками
Функция ОтправитьДанныеВоВнешнююСистему(Данные)МаксимумПопыток = 3;Для НомерПопытки = 1 По МаксимумПопыток ЦиклПопыткаHTTPСоединение = Новый HTTPСоединение("api.example.com",,,,, 30);Запрос = Новый HTTPЗапрос("/api/data");Запрос.УстановитьТелоИзСтроки(Данные);Ответ = HTTPСоединение.ОтправитьДляОбработки(Запрос);Если Ответ.КодСостояния = 200 ТогдаВозврат Истина;ИначеВызватьИсключение СтрШаблон(НСтр("ru = 'Сервер вернул код %1: %2'"),Ответ.КодСостояния,Ответ.ПолучитьТелоКакСтроку());КонецЕсли;ИсключениеЗаписьЖурналаРегистрации(НСтр("ru = 'Интеграция.ОтправкаДанных'"),?(НомерПопытки < МаксимумПопыток,УровеньЖурналаРегистрации.Предупреждение,УровеньЖурналаРегистрации.Ошибка),,,СтрШаблон(НСтр("ru = 'Попытка %1 из %2. Ошибка: %3'"),НомерПопытки, МаксимумПопыток,ПодробноеПредставлениеОшибки(ИнформацияОбОшибке())));Если НомерПопытки = МаксимумПопыток ТогдаВызватьИсключение;КонецЕсли;КонецПопытки;КонецЦикла;Возврат Ложь;КонецФункции
depends_on: []