Обработка ошибок
Макрос, который падает с окном «Run-time error '9'», пугает пользователя сильнее, чем любая опечатка в отчёте. Учимся ловить ошибки и говорить с человеком по-человечески.
Ошибка выполнения (run-time error) — сбой, который случается не при написании кода, а во время его работы: файла нет, лист переименовали, в ячейке текст вместо числа. Предугадать всё нельзя — но можно перехватить.
Зачем это нужно
Представьте: вы написали макрос, который каждое утро собирает отчёт по продажам. Три недели он работал безупречно. На четвёртой коллега переименовал лист «Данные» в «Данные (новые)» — и в 9 утра бухгалтер увидела вот такое окно: Run-time error '9': Subscript out of range и три кнопки — End, Debug, Help. Она нажала Debug, попала в редактор VBA с жёлтой подсветкой строки, испугалась и закрыла Excel без сохранения.
Проблема не в том, что макрос сломался, — рано или поздно сломается любой. Проблема в том, как он сломался: техническим сообщением на английском, посреди работы, с открытой дверью в код. Обработка ошибок нужна ровно для трёх вещей:
- объяснить человеку, что случилось и что теперь делать («переименуйте лист обратно», «закройте файл в другой программе»);
- не оставить Excel в поломанном состоянии — если макрос отключил пересчёт формул и упал, книга так и останется «мёртвой»;
- не соврать: не показывать «Готово!», когда половина строк не посчиталась.
Вот честный пример из практики. Такой код падает на первой же строке, где в столбце B оказался ноль:
Sub ПересчётСкидок()
Dim i As Long
For i = 2 To 100
Cells(i, 3).Value = Cells(i, 1).Value / Cells(i, 2).Value
Next i
MsgBox "Готово!"
End Sub
Excel остановит макрос на строке деления и покажет Run-time error '11': Division by zero. Первые двадцать строк уже записаны, остальные — нет, а пользователь не знает, можно ли доверять результату.
On Error GoTo: рабочий шаблон
Инструкция On Error GoTo Метка говорит VBA: «если дальше в этой процедуре что-то сломается — не показывай своё страшное окно, а прыгни на метку и выполняй код оттуда». Метка — это просто имя со двоеточием в конце.
Sub СохранитьОтчёт()
Dim путь As String
On Error GoTo Ошибка ' с этой строки все сбои идут на метку "Ошибка"
путь = "C:\Отчёты\" & Format(Date, "yyyy-mm-dd") & ".xlsx"
ThisWorkbook.SaveCopyAs путь
MsgBox "Отчёт сохранён: " & путь, vbInformation, "Готово"
Exit Sub ' ВАЖНО: без этой строки код "провалится" в блок ошибки
Ошибка:
MsgBox "Не удалось сохранить отчёт." & vbCrLf & _
"Проверьте, что папка C:\Отчёты существует и файл не открыт в другой программе." & _
vbCrLf & vbCrLf & _
"Техническая информация: ошибка " & Err.Number & " — " & Err.Description, _
vbExclamation, "Сохранение отчёта"
End Sub
Разберём построчно:
On Error GoTo Ошибка— включает перехват. Ставим его в самом начале, до первой «опасной» строки.- Дальше идёт обычная работа. Если всё хорошо, мы доходим до
Exit Subи выходим. Exit Subперед меткой — обязателен. Без него код после успешной работы просто «провалится» в блокОшибка, и пользователь увидит сообщение о сбое там, где сбоя не было. Это ошибка номер один у новичков.Ошибка:— метка. Всё, что ниже, выполняется только при сбое.Err— встроенный объект с описанием того, что случилось:Err.Number(код, например 1004) иErr.Description(текст от Excel).
Обратите внимание на сообщение. Оно состоит из двух частей: человеческой («не удалось… проверьте…») и технической («ошибка 1004 — Method SaveCopyAs of object _Workbook failed»). Первая нужна бухгалтеру, вторая — вам, когда бухгалтер пришлёт скриншот. Не выбрасывайте техническую часть и не показывайте её вместо человеческой.
Почему On Error Resume Next — зло
В интернете вам обязательно посоветуют: «добавь On Error Resume Next, и ругаться перестанет». Это правда — ругаться перестанет. Вместе со всеми предупреждениями о том, что расчёт неверный.
Sub ПересчётСкидок_ЗлоеРешение()
On Error Resume Next ' "чтобы не ругалось"
Dim i As Long
For i = 2 To 100
Cells(i, 3).Value = Cells(i, 1).Value / Cells(i, 2).Value
Next i
MsgBox "Готово!" ' покажется ВСЕГДА, даже если ничего не посчиталось
End Sub
On Error Resume Next означает: «при любой ошибке молча перейди к следующей строке». В нашем цикле это работает так: на строке, где в B стоит ноль, запись в C просто не происходит — а в C остаётся старое значение с прошлого запуска. Макрос бодро досчитывает до конца и рапортует «Готово!». Отчёт уходит директору. Ошибку находят через месяц, в сверке.
Разница принципиальная:
On Error GoTo— это «поймать и разобраться»,On Error Resume Next— «зажмуриться и ехать дальше». Первое делает программу надёжной, второе — молча лживой.
Второе коварство: Resume Next глушит все ошибки до конца процедуры, а не только ту, которой вы боялись. Опечатались в имени листа десятью строками ниже — тоже не узнаете.
Единственное честное применение — когда вы проверяете что-то, чего может не быть, и тут же возвращаете перехват обратно:
Dim лист As Worksheet
On Error Resume Next
Set лист = ThisWorkbook.Worksheets("Данные")
On Error GoTo 0 ' сразу выключаем "глушилку"
If лист Is Nothing Then
MsgBox "В книге нет листа «Данные».", vbExclamation
Exit Sub
End If
Здесь «глушилка» живёт ровно одну строку: On Error GoTo 0 отключает её сразу же. И даже в этом случае честнее написать явную проверку — к ней и переходим.
Проверять входные данные до работы
Лучшая обработка ошибки — та, которой не понадобилось. Прежде чем что-то считать, убедитесь, что исходные данные на месте и годные. В программировании такие проверки в начале процедуры называют защитными условиями (guard clauses): каждая проверяет одно условие и при неудаче сразу выходит с понятным сообщением.
Function ЛистЕсть(ByVal имя As String) As Boolean
Dim л As Worksheet
For Each л In ThisWorkbook.Worksheets
If StrComp(л.Name, имя, vbTextCompare) = 0 Then
ЛистЕсть = True
Exit Function
End If
Next л
End Function
Sub ПосчитатьПремию()
' --- ПРОВЕРКИ ДО РАБОТЫ ---
If Not ЛистЕсть("Данные") Then
MsgBox "В книге нет листа «Данные». Откройте нужный файл и повторите.", vbExclamation
Exit Sub
End If
Dim лист As Worksheet
Set лист = ThisWorkbook.Worksheets("Данные")
If лист.Range("A2").Value = "" Then
MsgBox "Лист «Данные» пуст — считать нечего.", vbExclamation
Exit Sub
End If
If Not IsNumeric(лист.Range("B1").Value) Then
MsgBox "В ячейке B1 должен быть процент премии (число), а сейчас там: " & _
лист.Range("B1").Value, vbExclamation
Exit Sub
End If
' --- и только теперь основная работа ---
ПересчитатьПремию лист
End Sub
Что мы проверили: лист существует, данные не пустые, коэффициент — число, а не текст «10%» или «десять». Три коротких If экономят десятки писем «у меня не работает». Полезные встроенные проверки: IsNumeric (число ли), IsDate (дата ли), IsEmpty (пустая ли ячейка), Dir(путь) <> "" (есть ли файл).
Заметьте: ЛистЕсть обходится вообще без обработки ошибок — мы просто перебираем листы. Это лучше, чем пытаться обратиться к листу и ловить сбой. Правило простое: ошибками ловим непредсказуемое (диск, сеть, чужие файлы), проверками — предсказуемое (что ввёл пользователь).
Как это работает
Перехват ошибок в VBA живёт внутри одной процедуры. Строка On Error GoTo Метка включает обработчик для текущей Sub или Function; на другие процедуры он не распространяется. Но есть важный нюанс: если в процедуре Б обработчика нет, а её вызвала процедура А с обработчиком, — ошибка «всплывёт» в А и будет поймана там. Поэтому достаточно защитить «точки входа» — макросы, которые запускает пользователь.
Когда ошибка случилась, VBA заполняет объект Err и переходит на метку. Пока вы находитесь в блоке обработчика, вы «внутри ошибки» — и у вас есть три способа из него выйти:
| Инструкция | Что делает |
Exit Sub | просто завершить процедуру |
Resume Метка | прыгнуть на метку (обычно — в блок уборки) |
Resume | повторить строку, которая упала (для «попробовать ещё раз») |
Resume Next | продолжить со следующей строки — осознанно, а не «на всякий случай» |
Отсюда вырастает главный производственный шаблон — с блоком уборки, который выполняется в любом случае:
Sub ОбновитьОтчёт()
On Error GoTo Ошибка
Application.ScreenUpdating = False
' ... долгая работа: чтение, пересчёт, запись ...
Завершение: ' сюда приходим и при успехе, и после ошибки
Application.ScreenUpdating = True
Exit Sub
Ошибка:
MsgBox "Отчёт обновить не удалось, данные не изменены." & vbCrLf & vbCrLf & _
"Ошибка " & Err.Number & ": " & Err.Description, vbExclamation
Resume Завершение ' возвращаемся в блок уборки
End Sub
Зачем это нужно, станет совсем ясно в следующем уроке: там мы ради скорости будем отключать пересчёт формул и перерисовку экрана. Если макрос упадёт, не включив их обратно, пользователь получит Excel, где формулы не считаются, — и будет уверен, что «программа сломала файл».
Частые ошибки
- Забыли
Exit Subперед меткой. Макрос отработал успешно — и всё равно показал «Не удалось сохранить отчёт». On Error Resume Nextв начале модуля «чтобы не мешало». Так вы не убираете ошибки, вы убираете сообщения о них.- Показ
Err.Descriptionвместо человеческого текста. «Application-defined or object-defined error» не говорит пользователю ровно ничего. - Пустой обработчик (
Ошибка:и сразуEnd Sub). То жеResume Next, только выглядит солиднее. - Не восстановили состояние Excel. Пересчёт остался ручным, события выключены, экран не обновляется.
- Показали «Готово!» после провала. Сообщение об успехе должно стоять до
Exit Sub, а не в конце процедуры. - Проверка «на бумаге». Обработчик надо тестировать: переименуйте лист, вставьте текст в числовой столбец, закройте доступ к папке — и посмотрите, что увидит пользователь.
Итоги
On Error GoTo Метка+Exit Subперед меткой — базовый шаблон надёжного макроса.Err.NumberиErr.Description— техническая часть сообщения, но не всё сообщение: сначала объясните человеку, что делать.On Error Resume Nextглушит все ошибки подряд и превращает сбой в тихо неверные цифры. Допустим только точечно и с немедленнымOn Error GoTo 0.- Предсказуемое проверяйте заранее:
IsNumeric,IsDate, наличие листа, непустой диапазон. Защитные условия в начале процедуры дешевле любого обработчика. - Блок уборки +
Resume Завершениегарантируют, что Excel вернётся в нормальное состояние даже после падения.