Строка --- 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() — этот механизм специально создан для сохранения полного стека. Проверьте в коде следующие моменты:

☑️ Проверка кода на потерю стека

Выполнено: 0 / 5

Отдельный нюанс — номера строк. Если в стеке видны только имена методов без строк, значит рядом с исполняемым файлом нет файлов символов .pdb. Для отладочных сборок они генерируются автоматически; для релизных их стоит сохранять при публикации, иначе локализация сбоя сильно затрудняется.

⚠️ Внимание: замена throw ex; на throw; меняет наблюдаемый стек исключения. Если внешний код или библиотека как-то полагаются на конкретное содержимое стека (что само по себе плохая практика), проверьте поведение после правки на тестовом стенде.
📊 Где вы чаще всего встречаете эту строку?
В логах серверного приложения (ASP.NET)
В десктопной программе (WPF/WinForms)
В консольной утилите или службе Windows
Я пользователь, а не разработчик

Сравнение способов переброса исключения

Разные способы повторного выброса по-разному влияют на содержимое стека. Сводная картина:

СпособЧто происходит со стекомРазделитель в выводе
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; сбрасывает стек и делает текущую точку «началом» исключения, что затрудняет поиск реальной причины. В коде почти всегда нужен первый вариант.