Обработка ошибок

Макрос, который падает с окном «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 вернётся в нормальное состояние даже после падения.
Проверьте себя
1. Почему в шаблоне On Error GoTo обязательно нужен Exit Sub перед меткой обработчика?
AИначе VBA не скомпилирует процедуру
BИначе после успешного выполнения код «провалится» в блок обработчика и покажет сообщение об ошибке, которой не было
CИначе обработчик сработает только один раз
DExit Sub нужен только в функциях, в Sub он не обязателен
2. Что произойдёт с макросом, который делит A на B в цикле, если поставить в начало On Error Resume Next и в столбце B встретится ноль?
AМакрос остановится на этой строке и покажет ошибку 11
BВ ячейку запишется 0
CExcel автоматически заменит ноль на единицу
DПроблемная строка будет молча пропущена, в результате останется старое значение, а макрос отрапортует об успехе