framework/skills/bsl-practices/error-handling/SKILL.md
MUST use WHEN обрабатываешь исключения или управляешь транзакциями в BSL. Provides каноническую схему Попытка/Исключение, правила отката транзакций и управления блокировками данных.
npx skillsauth add steelmorgan/1c-agent-based-dev-framework error-handlingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Ключевой принцип: В 1С нет автоматического управления транзакциями. Разработчик вручную управляет началом, фиксацией и откатом. Каждая незакрытая транзакция — потенциальная катастрофа.
Проглоченное исключение — самый опасный антипаттерн: данные не записаны, пользователь не знает об ошибке, в ЖР нет следов, отладка невозможна.
Стандарт ИТС: «В обработчике исключения обязательно фиксировать информацию об ошибке в журнале регистрации».
Попытка
ДокументОбъект.Записать(РежимЗаписиДокумента.Проведение);
Исключение
ИнформацияОбОшибке = ИнформацияОбОшибке();
ЗаписьЖурналаРегистрации(
НСтр("ru = 'Проведение документа'"),
УровеньЖурналаРегистрации.Ошибка,
ДокументОбъект.Метаданные(),
ДокументОбъект.Ссылка,
ПодробноеПредставлениеОшибки(ИнформацияОбОшибке));
ВызватьИсключение;
КонецПопытки;
| Функция | Когда использовать | Что содержит |
|---------|-------------------|-------------|
| КраткоеПредставлениеОшибки() | Для отображения пользователю | Понятный текст без технических деталей |
| ПодробноеПредставлениеОшибки() | Для журнала регистрации | Полный стек вызовов, номера строк, вложенные ошибки |
// Нижний уровень — логирование + проброс
Функция ЗаписатьДокумент(ДокументОбъект)
Попытка
ДокументОбъект.Записать(РежимЗаписиДокумента.Проведение);
Возврат Истина;
Исключение
ЗаписьЖурналаРегистрации(
НСтр("ru = 'Проведение документа'"),
УровеньЖурналаРегистрации.Ошибка,
ДокументОбъект.Метаданные(),
ДокументОбъект.Ссылка,
ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
ВызватьИсключение;
КонецПопытки;
КонецФункции
// Верхний уровень (форма) — показ пользователю
&НаКлиенте
Процедура ЗаписатьДокумент(Команда)
Попытка
ЗаписатьДокументНаСервере();
Исключение
ПоказатьПредупреждение(,
НСтр("ru = 'Не удалось записать документ. Обратитесь к администратору.'"));
КонецПопытки;
КонецПроцедуры
Незакрытая транзакция блокирует записи в СУБД. Другие сеансы ждут (таймаут ~20 сек) и получают ошибку.
Стандарт ИТС: «Транзакции: правила использования» — НачатьТранзакцию() ВСЕГДА непосредственно перед Попытка.
НачатьТранзакцию();
Попытка
// 1. Блокировка данных (если нужно — см. правило 5)
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("Документ.РеализацияТоваровУслуг");
ЭлементБлокировки.УстановитьЗначение("Ссылка", ДокументСсылка);
Блокировка.Заблокировать();
// 2. Чтение и модификация данных
ДокументОбъект = ДокументСсылка.ПолучитьОбъект();
ДокументОбъект.Статус = Перечисления.СтатусыДокументов.Согласован;
// 3. Запись
ДокументОбъект.Записать();
// === Фиксация — ПОСЛЕДНЯЯ операция перед Исключение ===
ЗафиксироватьТранзакцию();
Исключение
// === Откат — ПЕРВАЯ операция в блоке Исключение ===
ОтменитьТранзакцию();
// Логирование ПОСЛЕ отката (запись в ЖР внутри отменённой транзакции будет потеряна!)
ЗаписьЖурналаРегистрации(
НСтр("ru = 'Согласование документа'"),
УровеньЖурналаРегистрации.Ошибка,
Метаданные.Документы.РеализацияТоваровУслуг,
ДокументСсылка,
ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
ВызватьИсключение;
КонецПопытки;
| Требование | Почему |
|------------|--------|
| НачатьТранзакцию() непосредственно перед Попытка | Если между ними ошибка — транзакция не закроется |
| ЗафиксироватьТранзакцию() — последний оператор перед Исключение | Операция после фиксации при ошибке не откатится |
| ОтменитьТранзакцию() — первый оператор в Исключение | Логирование тоже может вызвать ошибку; если откат ещё не сделан — каскадная проблема |
| ЗаписьЖурналаРегистрации() — ПОСЛЕ ОтменитьТранзакцию() | Запись в ЖР внутри отменённой транзакции будет потеряна |
// ПЛОХО: код между НачатьТранзакцию и Попытка
НачатьТранзакцию();
ПодготовитьДанные(); // Если здесь ошибка — транзакция зависнет!
Попытка
// ...
КонецПопытки;
// ПЛОХО: ЗаписьЖурнала ДО ОтменитьТранзакцию
Исключение
ЗаписьЖурналаРегистрации(...); // Может быть потеряна при откате!
ОтменитьТранзакцию();
КонецПопытки;
// ПЛОХО: код после ЗафиксироватьТранзакцию, но до конца Попытка
ЗафиксироватьТранзакцию();
ОтправитьОповещение(); // Ошибка здесь — транзакция уже зафиксирована, но Исключение выполнится!
Исключение
ОтменитьТранзакцию(); // Ошибка! Транзакция уже зафиксирована!
КонецПопытки;
В 1С вложенный НачатьТранзакцию() не создаёт новую транзакцию, а увеличивает счётчик. ОтменитьТранзакцию() помечает транзакцию как «отменённая», и любой последующий ЗафиксироватьТранзакцию() (даже на внешнем уровне) вызовет исключение.
Процедура ЗаписатьДанные(ДанныеДляЗаписи)
НачатьТранзакцию();
Попытка
ДокументОбъект.Записать();
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ЗаписьЖурналаРегистрации(
НСтр("ru = 'Запись данных'"),
УровеньЖурналаРегистрации.Ошибка,,,
ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
ВызватьИсключение; // ОБЯЗАТЕЛЬНО пробрасываем — внешний код должен знать
КонецПопытки;
КонецПроцедуры
// ПЛОХО: ТранзакцияАктивна() маскирует ошибку в структуре кода
Попытка
НачатьТранзакцию();
// ...
ЗафиксироватьТранзакцию();
Исключение
Если ТранзакцияАктивна() Тогда
ОтменитьТранзакцию();
КонецЕсли;
КонецПопытки;
// ПРАВИЛЬНО: корректная структура делает проверку ненужной
НачатьТранзакцию();
Попытка
// ...
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ВызватьИсключение;
КонецПопытки;
Пока транзакция открыта — изменённые данные заблокированы в СУБД. Длинная транзакция = каскадные блокировки = пользователи не могут работать.
// Подготовка данных — ВНЕ транзакции
МассивДанных = ПодготовитьДанные();
ПроверитьКорректность(МассивДанных);
// Транзакция — только быстрые операции записи
НачатьТранзакцию();
Попытка
Для Каждого ДанныеСтроки Из МассивДанных Цикл
ЗаписатьСтроку(ДанныеСтроки);
КонецЦикла;
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ЗаписьЖурналаРегистрации(...);
ВызватьИсключение;
КонецПопытки;
Внешние HTTP-вызовы внутри транзакции — катастрофа: таймаут HTTP = 30 сек = блокировка 30 сек.
Без гранулярной блокировки перед чтением-модификацией возникает 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 → Ошибка (корректная!)
Предотвращает lost update: второй пользователь получит ошибку «Объект заблокирован пользователем X».
Процедура ИзменитьСтатусДокумента(ДокументСсылка, НовыйСтатус)
НачатьТранзакцию();
Попытка
ЗаблокироватьДанныеДляРедактирования(ДокументСсылка);
ДокументОбъект = ДокументСсылка.ПолучитьОбъект();
ДокументОбъект.Статус = НовыйСтатус;
ДокументОбъект.Записать();
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ЗаписьЖурналаРегистрации(
НСтр("ru = 'Изменение статуса документа'"),
УровеньЖурналаРегистрации.Ошибка,,,
ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
ВызватьИсключение;
КонецПопытки;
КонецПроцедуры
| Тип блокировки | Механизм | Когда использовать |
|----------------|----------|-------------------|
| БлокировкаДанных | СУБД (управляемая), уровень записей | Контроль остатков, атомарные операции |
| ЗаблокироватьДанныеДляРедактирования | Сервер 1С (пессимистичная), объект целиком | Предотвращение lost update |
ЗаписьЖурналаРегистрации(
ИмяСобытия, // Строка — иерархическое имя (через точку)
УровеньСобытия, // УровеньЖурналаРегистрации — Ошибка/Предупреждение/Информация/Примечание
МетаданныеОбъекта, // Объект метаданных — для фильтрации по типу
Данные, // Ссылка на объект — для навигации из ЖР
Комментарий); // Строка — подробное описание (до 1024 символов)
| Уровень | Когда |
|---------|-------|
| Ошибка | Операция не выполнена, данные потеряны или некорректны |
| Предупреждение | Операция выполнена, но с ограничениями |
| Информация | Значимые события для аудита |
| Примечание | Диагностическая информация |
ИмяСобытия = НСтр("ru = 'ОбменДанными.ОтправкаДанных.Ошибка'");
Комментарий = СтрШаблон(
НСтр("ru = 'Ошибка при отправке данных в узел ""%1"".
|Количество объектов: %2.
|Текст ошибки:
|%3'"),
Строка(УзелОбмена),
КоличествоОбъектов,
ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
ЗаписьЖурналаРегистрации(
ИмяСобытия,
УровеньЖурналаРегистрации.Ошибка,
Метаданные.ПланыОбмена.ОбменСКонтрагентами,
УзелОбмена,
Комментарий);
Пользователю — что произошло и что делать. В ЖР — техническую информацию.
Попытка
ДокументОбъект.Записать(РежимЗаписиДокумента.Проведение);
Исключение
ЗаписьЖурналаРегистрации(
НСтр("ru = 'Проведение документа'"),
УровеньЖурналаРегистрации.Ошибка,
ДокументОбъект.Метаданные(),
ДокументОбъект.Ссылка,
ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
ТекстДляПользователя = СтрШаблон(
НСтр("ru = 'Не удалось провести документ ""%1"".
|Попробуйте повторить операцию. Если ошибка повторяется, обратитесь к администратору.
|
|Техническая информация: %2'"),
ДокументОбъект,
КраткоеПредставлениеОшибки(ИнформацияОбОшибке()));
ВызватьИсключение ТекстДляПользователя;
КонецПопытки;
| Способ | Когда | Почему |
|--------|-------|--------|
| ВызватьИсключение; | В промежуточном коде (модуль объекта, общий модуль) | Сохраняет оригинальный стек |
| ВызватьИсключение "Текст"; | На границе с пользователем (форма) | Заменяет технический стек на понятное сообщение |
// Промежуточный слой — пробрасываем оригинал
Процедура ОбработатьДанные(Данные)
Попытка
ЗаписатьДанные(Данные);
Исключение
ЗаписьЖурналаРегистрации(...);
ВызватьИсключение; // Оригинальный стек сохранён
КонецПопытки;
КонецПроцедуры
// Граница с пользователем
&НаСервере
Процедура ОбработатьНаСервере()
Попытка
ОбработатьДанные(ДанныеФормы);
Исключение
ВызватьИсключение СтрШаблон(
НСтр("ru = 'Ошибка обработки данных: %1'"),
КраткоеПредставлениеОшибки(ИнформацияОбОшибке()));
КонецПопытки;
КонецПроцедуры
Ошибка в одном документе не должна останавливать обработку остальных. Каждая транзакция — поэлементная.
Процедура ПровестиДокументыПакетно(МассивДокументов)
МассивОшибок = Новый Массив;
Для Каждого ДокументСсылка Из МассивДокументов Цикл
НачатьТранзакцию();
Попытка
ДокументОбъект = ДокументСсылка.ПолучитьОбъект();
ДокументОбъект.Записать(РежимЗаписиДокумента.Проведение);
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ИнфоОшибки = ИнформацияОбОшибке();
ЗаписьЖурналаРегистрации(
НСтр("ru = 'Пакетное проведение'"),
УровеньЖурналаРегистрации.Ошибка,
ДокументСсылка.Метаданные(),
ДокументСсылка,
ПодробноеПредставлениеОшибки(ИнфоОшибки));
МассивОшибок.Добавить(Новый Структура("Документ, Ошибка",
ДокументСсылка,
КраткоеПредставлениеОшибки(ИнфоОшибки)));
КонецПопытки;
КонецЦикла;
Если МассивОшибок.Количество() > 0 Тогда
Для Каждого ОписаниеОшибки Из МассивОшибок Цикл
ОбщегоНазначения.СообщитьПользователю(
СтрШаблон(НСтр("ru = 'Документ %1: %2'"),
ОписаниеОшибки.Документ, ОписаниеОшибки.Ошибка));
КонецЦикла;
КонецЕсли;
КонецПроцедуры
Когда два сеанса блокируют данные в разном порядке — deadlock. СУБД откатывает одну из транзакций.
// Всегда блокируйте ресурсы в фиксированном порядке (по ссылке)
МассивСсылок = ОбщегоНазначенияКлиентСервер.СвернутьМассив(МассивДокументов);
МассивСсылок.СортироватьПоЗначению();
Для Каждого Ссылка Из МассивСсылок Цикл
ЗаблокироватьДанныеДляРедактирования(Ссылка);
КонецЦикла;
НачатьТранзакцию();
Попытка
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("Справочник.Номенклатура");
ЭлементБлокировки.УстановитьЗначение("Ссылка", НоменклатураСсылка);
Попытка
Блокировка.Заблокировать();
Исключение
ОтменитьТранзакцию();
ВызватьИсключение СтрШаблон(
НСтр("ru = 'Не удалось заблокировать ""%1"". Данные редактируются другим пользователем.'"),
НоменклатураСсылка);
КонецПопытки;
НоменклатураОбъект = НоменклатураСсылка.ПолучитьОбъект();
НоменклатураОбъект.Записать();
ЗафиксироватьТранзакцию();
Исключение
Если ТранзакцияАктивна() Тогда
ОтменитьТранзакцию();
КонецЕсли;
ЗаписьЖурналаРегистрации(...);
ВызватьИсключение;
КонецПопытки;
Вызовы внешних систем ненадёжны. Всегда оборачивайте в Попытка/Исключение.
Функция ОтправитьДанныеВоВнешнююСистему(Данные)
МаксимумПопыток = 3;
Для НомерПопытки = 1 По МаксимумПопыток Цикл
Попытка
HTTPСоединение = Новый HTTPСоединение("api.example.com",,,,, 30);
Запрос = Новый HTTPЗапрос("/api/data");
Запрос.УстановитьТелоИзСтроки(Данные);
Ответ = HTTPСоединение.ОтправитьДляОбработки(Запрос);
Если Ответ.КодСостояния = 200 Тогда
Возврат Истина;
Иначе
ВызватьИсключение СтрШаблон(
НСтр("ru = 'Сервер вернул код %1: %2'"),
Ответ.КодСостояния,
Ответ.ПолучитьТелоКакСтроку());
КонецЕсли;
Исключение
ЗаписьЖурналаРегистрации(
НСтр("ru = 'Интеграция.ОтправкаДанных'"),
?(НомерПопытки < МаксимумПопыток,
УровеньЖурналаРегистрации.Предупреждение,
УровеньЖурналаРегистрации.Ошибка),,,
СтрШаблон(НСтр("ru = 'Попытка %1 из %2. Ошибка: %3'"),
НомерПопытки, МаксимумПопыток,
ПодробноеПредставлениеОшибки(ИнформацияОбОшибке())));
Если НомерПопытки = МаксимумПопыток Тогда
ВызватьИсключение;
КонецЕсли;
КонецПопытки;
КонецЦикла;
Возврат Ложь;
КонецФункции
testing
MUST use BEFORE making a judgment about the cause of a conflict, a test failure, or an artifact dispute. Defines the end-to-end verification method L1→L6 and the classification of the first broken link.
development
MUST use AFTER a work cycle with ≥2 iterations (wrote → error → fixed → success). Provides the retrospective procedure and the format for recording practice/anti-patterns in references/learned-patterns.md or {project}/.context/learned-patterns.md.
tools
MUST use WHEN you are writing reusable knowledge into RLM (pattern / architectural decision / stable domain fact) OR reading it before a non-trivial task/solution in the domain. Provides the breakdown of native-push vs RLM-pull, tools for writing and reading RLM, H-MEM levels, and hygiene.
testing
MUST use WHEN the task is classified as simple (< 20 lines, 1 file, no new metadata objects, no architectural decisions). Provides a short cycle of 3 steps with a guard on the self path and mandatory verify.