Строка --- End of stack trace from previous location where exception was thrown --- (в русской локализации — «конец трассировки стека из предыдущего расположения, где возникло исключение») появляется в тексте исключения .NET-приложения и не является отдельной ошибкой: это разделитель, который среда выполнения вставляет в StackTrace, когда исключение перебрасывается через границы асинхронных вызовов или повторно генерируется через throw;. Именно поэтому многие разработчики и системные администраторы, впервые увидев её в логе или в окне ошибки, принимают её за саму проблему, хотя реальная причина находится выше или ниже этой строки в тексте исключения.
Разберёмся, откуда берётся эта надпись, почему она затрудняет чтение стека, как правильно анализировать такие исключения в приложениях на C# и что делать, если вы просто пользователь программы, а не её разработчик.
Что означает эта строка на самом деле
Когда в .NET возникает исключение, в объект Exception записывается стек вызовов — цепочка методов, через которые прошло выполнение до точки сбоя. Если исключение «путешествует» через асинхронный код (async/await), задачи Task или повторно выбрасывается, среда выполнения дополняет стек и вставляет маркер-разделитель, чтобы показать: дальше идёт стек предыдущего места, где это же исключение уже было выброшено.
Иными словами, строка говорит: «всё, что ниже, — это история исключения до того, как оно попало в текущую точку». Сама по себе она не содержит диагностики и не указывает ни на какой сбойный компонент. Важно понимать, что искать причину нужно в типе исключения, его сообщении (Message) и кадрах стека вокруг разделителя, а не в самой строке-разделителе.
Строка «конец трассировки стека…» — это не ошибка, а разделитель внутри стека исключения. Реальная причина указана в типе исключения и сообщении выше неё.
Типичные сценарии, в которых появляется разделитель
Чаще всего такая строка встречается в логах серверных приложений на ASP.NET, в консольных утилитах, фоновых службах Windows и десктопных программах на WPF или WinForms, написанных на C#. Типичные ситуации:
- 🔄 Исключение проброшено через
await— асинхронный метод завершился ошибкой, и стек «сшит» из двух частей. - 🔁 Код использует
ExceptionDispatchInfo.Throwили повторныйthrow;для передачи исключения между потоками. - 🧵 Ошибка возникла в
Task.Run, таймере или обработчике события и была получена в другом потоке. - 📦 Исключение сериализовано и переброшено между процессами или через gRPC/WCF-вызов.
Если вы видите эту строку в логе рабочего приложения, это признак того, что где-то в коде необработанное исключение прошло через асинхронную границу. Сам по себе разделитель безвреден, но он сигнализирует о реальном сбое, который нужно найти.
Как правильно читать такой стек
Правильный порядок чтения исключения с разделителем выглядит так. Сначала смотрите на тип исключения и текст Message — они в самом верху. Затем читайте кадры стека сверху вниз до разделителя: это путь от текущей точки перехвата до места переброса. После разделителя идёт «исторический» стек — там и находится исходная точка, где исключение было выброшено впервые, и именно эти кадры обычно самые ценные для диагностики.
System.NullReferenceException: Object reference not set to an instance of an object.
at MyApp.Services.OrderService.LoadOrder(Int32 id)
--- End of stack trace from previous location ---
at MyApp.Controllers.OrderController.Get(Int32 id)
В этом примере реальный сбой произошёл в методе LoadOrder — он указан выше разделителя, потому что там исключение было зафиксировано последним. Ниже разделителя видно, откуда вызов пришёл. При чтении вложенных исключений не забывайте раскрывать цепочку InnerException: часто первопричина спрятана именно там, на два-три уровня глубже.
При анализе логов ищите первое вхождение типа исключения и строку с именем вашего кода (а не системных сборок) — это почти всегда и есть точка сбоя.
Почему стек теряется и как его сохранить
Классическая ошибка в C# — переброс исключения через throw ex; вместо throw;. В первом случае стек обрезается: теряется исходная точка возникновения, и диагностика превращается в гадание. Во втором случае стек сохраняется, а разделитель как раз и показывает, где исключение прошло границу.
Для асинхронного кода и межпоточной передачи рекомендуется использовать ExceptionDispatchInfo.Capture(ex).Throw() — этот механизм специально создан для сохранения полного стека. Проверьте в коде следующие моменты:
☑️ Проверка кода на потерю стека
Отдельный нюанс — номера строк. Если в стеке видны только имена методов без строк, значит рядом с исполняемым файлом нет файлов символов .pdb. Для отладочных сборок они генерируются автоматически; для релизных их стоит сохранять при публикации, иначе локализация сбоя сильно затрудняется.
⚠️ Внимание: заменаthrow ex;наthrow;меняет наблюдаемый стек исключения. Если внешний код или библиотека как-то полагаются на конкретное содержимое стека (что само по себе плохая практика), проверьте поведение после правки на тестовом стенде.
Сравнение способов переброса исключения
Разные способы повторного выброса по-разному влияют на содержимое стека. Сводная картина:
| Способ | Что происходит со стеком | Разделитель в выводе |
|---|---|---|
throw; | Исходный стек сохраняется полностью | Может присутствовать |
throw ex; | Стек обрезается до текущей точки | Обычно нет, но история теряется |
ExceptionDispatchInfo.Throw() | Стек сохраняется, добавляется маркер | Да, появляется явно |
Ошибка через await | Стек «сшивается» из двух частей | Да, типичный случай |
| Обёртка в новое исключение | Исходный стек уходит в InnerException | Зависит от реализации |
Из таблицы видно, что наличие разделителя — скорее хороший знак: он означает, что стек не потерян, а дополнен. Куда хуже, когда разделителя нет, а стек подозрительно короткий — это намёк на throw ex; где-то по пути.
Что делать, если вы пользователь, а не разработчик
Если окно с таким текстом появилось в программе, которую вы просто используете, самостоятельно исправить причину обычно нельзя — сбой находится в коде приложения. Однако есть разумные шаги, не требующие вмешательства в программу:
- 📋 Скопируйте или сфотографируйте весь текст ошибки целиком, включая строки до и после разделителя.
- 🔁 Проверьте, повторяется ли сбой при одном и том же действии — это ценная информация для разработчика.
- 🔄 Обновите программу до актуальной версии: подобные необработанные исключения часто исправляют в патчах.
- 🗂️ Загляните в «Просмотр событий» Windows (
eventvwr.msc), раздел «Журналы Windows → Приложение» — там может быть более полная запись об ошибке .NET Runtime.
⚠️ Внимание: не пытайтесь «лечить» такую ошибку очисткой реестра, сторонними «оптимизаторами» или переустановкой .NET наугад. Разделитель в стеке — не признак повреждения системы, а содержимое конкретного исключения конкретной программы. Переустановка имеет смысл только если сама платформа .NET повреждена, что подтверждается ошибками запуска множества приложений сразу.
Как включить подробный вывод исключений в своём приложении
Используйте ex.ToString() вместо ex.Message при логировании — это выводит тип, сообщение, полный стек и все InnerException. Для ASP.NET настройте логирование через ILogger и убедитесь, что необработанные исключения попадают в middleware обработки ошибок. Файлы .pdb для релизных сборок генерируйте в формате portable и храните вместе с артефактами сборки.
Диагностика в логах: практический подход
При массовом анализе логов удобно настроить поиск по типам исключений, а не по строке-разделителю — она встречается слишком часто и ничего не означает сама по себе. Ищите NullReferenceException, InvalidOperationException, SqlException и подобные конкретные типы, затем раскрывайте контекст вокруг найденного события.
Если логирование в вашем приложении выводит только ex.Message, вы теряете и стек, и вложенные исключения. Минимально полезный формат — ex.ToString(), а лучше — структурированное логирование с передачей самого объекта исключения в логгер. Тогда разделитель станет помощником: по нему сразу видно, что исключение пересекало асинхронную границу, и кадры после него укажут на истинный источник.
Полезная диагностика — это ex.ToString() и раскрытая цепочка InnerException. Сообщение без стека почти никогда не позволяет найти причину.
Частые вопросы
Эта строка — вирус или сбой Windows?
Нет. Это служебный текст, который платформа .NET вставляет в описание исключения конкретного приложения. К операционной системе и безопасности он отношения не имеет.
Можно ли отключить появление этой строки?
Отключить её штатной настройкой нельзя — она часть механизма формирования стека. Она перестанет появляться только если исключения не будут пересекать асинхронные границы или перебрасываться между потоками.
Почему в стеке нет номеров строк кода?
Номера строк берутся из файлов символов .pdb. Если их нет рядом с приложением или они не соответствуют версии сборки, стек покажет только имена методов. Для своих приложений сохраняйте символы при публикации.
Ошибка появляется только иногда, что делать?
Нерегулярность типична для асинхронного кода: гонки потоков, таймауты, внешние сервисы. Собирайте полные тексты исключений с метками времени и ищите закономерность — одно и то же действие, нагрузку, время суток. Для разработчика это основной материал для воспроизведения.
Чем отличается throw от throw ex?
throw; перебрасывает текущее исключение с сохранением исходного стека. throw ex; сбрасывает стек и делает текущую точку «началом» исключения, что затрудняет поиск реальной причины. В коде почти всегда нужен первый вариант.