Deja de Entrecerrar los Ojos con los Stack Traces de .NET: Depúralos con Claude en Visual Studio
Salta una excepción no controlada y Visual Studio te suelta cuarenta líneas de stack trace en el regazo. Treinta y ocho viven en Microsoft.EntityFrameworkCore.* o en System.Private.CoreLib, una es un frame de máquina de estados asíncrona con un nombre tipo <<Main>$>b__0_0, y exactamente una apunta a código que escribiste tú. Tu trabajo es encontrar esa línea, entender por qué explotó y arreglar la causa real — no el síntoma.
Todos hemos pegado un trace en el buscador y recibido a cambio una sola pregunta de Stack Overflow de 2015, cero respuestas, y un único comentario que dice "¿al final lo resolviste?". Hay una forma mejor, y ahora vive dentro de Visual Studio: le pasas el trace a Claude y recibes un diagnóstico ordenado por probabilidad, atado a tu línea, con un arreglo que de verdad puedes razonar.
Por Qué Cuesta Tanto Leer un Stack Trace de .NET
Los stack traces no son difíciles por ser largos. Son difíciles porque el código moderno de .NET casi nunca corre en línea recta, y el trace refleja la maquinaria, no tu intención.
-
Desenrollado asíncrono. Un
awaitcompila a una máquina de estados. Cuando una excepción cruza unawait, los frames que ves son llamadas aMoveNext()yExecutionContext.Run, no la cadena de llamadas ordenada que escribiste. El método aparece como<AlgoAsync>d__4, y la línea que lógicamente lanzó puede estar tresawaitmás arriba. -
Excepciones internas (inner exceptions). El mensaje de arriba suele ser el menos útil. La causa real está anidada — un
DbUpdateExceptionenvolviendo unSqliteException, o unTargetInvocationExceptionenvolviendo lo que de verdad importa. Si solo lees la primera línea, estás depurando el envoltorio. -
AggregateException. Cualquier cosa conTask.WhenAllo paralelismo puede empaquetar varios fallos en uno. El mensaje externo dice "Se produjeron uno o más errores" — gracias, utilísimo. - Frames inlineados y de framework. El JIT hace inline de métodos pequeños, así que hay frames que desaparecen. Y los que quedan son en su mayoría internos de librerías que no puedes cambiar ni necesitas leer.
Un LLM es genuinamente bueno para esta tarea concreta. Ha visto estas formas de excepción miles de veces, sabe separar el ruido del framework de la señal y — esto es lo clave — cuando corre dentro de tu IDE, puede correlacionar el trace con el archivo y la línea reales.
Dos Formas de Meter a Claude Dentro de Visual Studio
No necesitas salir del IDE ni pegar nada en un navegador. Hay dos caminos sólidos, y cada uno sirve para un momento distinto.
1. Copilot Chat con un modelo Claude (dentro del IDE, conversacional)
Visual Studio 2022 (17.14+) trae GitHub Copilot con un selector de modelo. Haz clic en el badge de Copilot, abre el desplegable de modelos en la caja del chat y elige un modelo Claude — Sonnet para el diagnóstico rápido del día a día, Opus cuando el bug es realmente feo. A partir de ahí, cada respuesta de Copilot Chat la genera Claude.
Dos cosas hacen que este camino brille para depurar:
- Cuando una excepción no controlada abre el Asistente de excepciones durante el debug, hay un enlace Preguntar a Copilot justo en ese diálogo. Con Claude seleccionado como modelo, ese análisis de un clic corre sobre Claude.
- En la caja del chat puedes traer contexto real con referencias
#—#Program.cs,#solution, o una selección resaltada — para que Claude lea tu código de verdad, no una paráfrasis.
Si prefieres usar tu propia clave de Anthropic en vez de los modelos de Copilot, el mismo selector tiene Manage Models → agregar Anthropic → pegar tu API key. Cómodo si ya pagas por acceso a la API.
2. Claude Code en la terminal integrada (agéntico)
Todavía no hay una extensión oficial de Claude Code para el Visual Studio completo, pero no la necesitas — Claude Code corre en cualquier terminal, incluida Ver → Terminal (Ctrl+) dentro de VS. Ejecutaclaude`, pega el trace y, como Claude Code puede leer todo el repositorio, abrirá él mismo el archivo culpable, seguirá la ruta de llamadas y propondrá el arreglo como un diff que apruebas o rechazas.
El intercambio es simple: Copilot Chat es más rápido para un "¿qué significa esto?", mientras que Claude Code es mejor cuando el arreglo toca varios archivos o quieres que reproduzca y verifique. Usa el que el bug se merezca.
Un Ejemplo Real: El Trace de EF Core Que Culpa a Quien No Es
Depuremos algo concreto. Este es un endpoint de minimal API en .NET 10 que devuelve el resumen de pedidos de un cliente. Se ve razonable, y compila sin quejas:
`csharp
// Program.cs — minimal API en .NET 10
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext(o => o.UseSqlite("Data Source=shop.db"));
var app = builder.Build();
app.MapGet("/orders/{customerId:int}/summary", async (int customerId, ShopContext db) =>
{
// Disparamos ambas consultas "en paralelo" para ir rápido — ¿verdad?
var ordersTask = db.Orders.Where(o => o.CustomerId == customerId).ToListAsync();
var totalTask = db.Orders.Where(o => o.CustomerId == customerId).SumAsync(o => o.Total);
await Task.WhenAll(ordersTask, totalTask);
return Results.Ok(new { Orders = ordersTask.Result, Total = totalTask.Result });
});
app.Run();
`
Llamas al endpoint y lanza. Este es el trace que muestra Visual Studio (recortado a los frames que aparecen):
text1 source, CancellationToken ct)
System.InvalidOperationException: A second operation was started on this context
instance before a previous operation completed. This is usually caused by different
threads concurrently using the same instance of DbContext. For more information on
how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.
at Microsoft.EntityFrameworkCore.Internal.ConcurrencyDetector.EnterCriticalSection()
at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.ExecuteAsync[TResult](Expression query, CancellationToken ct)
at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.ExecuteAsync[TResult](Expression expression, CancellationToken ct)
at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ToListAsync[TSource](IQueryable
at Program.<>c.<
`
Cuatro de esos cinco frames son internos de EF Core. El único que es tuyo — Program.cs:line 12 — es la llamada a ToListAsync(), lo que hace parecer que ToListAsync es el villano. No lo es.
Cómo pasárselo a Claude bien
La calidad de la respuesta depende por completo de lo que pegues. Un mensaje de una línea te da una conjetura de una línea. Dale a Claude el panorama completo:
`text
.NET 10, EF Core 10, SQLite. Este endpoint lanza en cada request con carga normal
(no solo con concurrencia). Abajo van la excepción completa y el código del endpoint —
¿cuál es la causa raíz y cuál es el arreglo idiomático?
[pega la excepción COMPLETA con ex.ToString() — mensaje + todas las inner exceptions + trace]
[pega el handler del MapGet]
`
Tres cosas hacen que ese prompt sea bueno:
-
La excepción completa, no el titular.
ex.ToString()incluye cada inner exception y el trace entero. En Copilot Chat,#Program.cste trae el código solo. -
El primer frame que es tuyo. Apunta a Claude a
Program.cs:line 12y al código a su alrededor. Los frames de framework son contexto; tu frame es la escena del crimen. - Las condiciones. "En cada request" vs "solo bajo carga" vs "intermitente" cambia el diagnóstico por completo. Di lo que observaste.
Lo que Claude te responde
En vez de "la línea 12 falló", obtienes el mecanismo real: el DbContext de EF Core no es thread-safe y no permite dos operaciones sobre la misma instancia a la vez. Task.WhenAll arranca la segunda consulta antes de que termine la primera, ambas sobre el mismo db inyectado, y el ConcurrencyDetector de EF lanza. El número de línea era una pista falsa — el bug es el patrón, no la llamada.
Y te da los dos arreglos, con el intercambio incluido:
csharp
// Arreglo A — el más simple: ejecútalas en secuencia. Un contexto, una operación a la vez.
var orders = await db.Orders.Where(o => o.CustomerId == customerId).ToListAsync();
var total = await db.Orders.Where(o => o.CustomerId == customerId).SumAsync(o => o.Total);
return Results.Ok(new { Orders = orders, Total = total });
`csharp
// Arreglo B — paralelismo real: dale a cada consulta su PROPIO contexto vía factory.
builder.Services.AddDbContextFactory(o => o.UseSqlite("Data Source=shop.db"));
app.MapGet("/orders/{customerId:int}/summary",
async (int customerId, IDbContextFactory factory) =>
{
await using var ordersDb = await factory.CreateDbContextAsync();
await using var totalDb = await factory.CreateDbContextAsync();
var ordersTask = ordersDb.Orders.Where(o => o.CustomerId == customerId).ToListAsync();
var totalTask = totalDb.Orders.Where(o => o.CustomerId == customerId).SumAsync(o => o.Total);
await Task.WhenAll(ordersTask, totalTask);
return Results.Ok(new { Orders = await ordersTask, Total = await totalTask });
});
`
El Arreglo A es lo que quieres el 90% de las veces — dos consultas a SQLite son rápidas, y el paralelismo nunca valió el bug. El Arreglo B es la respuesta cuando las consultas son genuinamente lentas e independientes, porque IDbContextFactory<T> te entrega un contexto fresco y de vida corta por operación. Esa distinción — no el número de línea — es lo que de verdad te faltaba, y es el tipo de criterio que te da una buena explicación y no un resultado de búsqueda.
Un stack trace asíncrono de .NET tiene dos tipos de frames: los cuarenta que le pertenecen al runtime, y el que es culpa tuya. El trabajo de Claude es saltarse los cuarenta y explicarte el uno.
Verifica — No Confíes a Ciegas
Claude es un colega senior muy rápido que de vez en cuando se equivoca con toda seguridad. Trata su diagnóstico como una hipótesis fuerte, no como un veredicto:
- Reproduce y confirma. Aplica el arreglo, vuelve a correr y comprueba que la excepción de verdad desapareció — idealmente bajo las mismas condiciones que la dispararon. En Visual Studio, pon un breakpoint en la línea corregida y observa el estado.
- Pregunta el por qué, no solo el qué. "Explica por qué esto lo arregla" saca a la luz una causa raíz equivocada rapidísimo. Si el razonamiento no se sostiene, el arreglo probablemente tampoco.
- Ojo con las causas plausibles pero falsas. Sobre todo con bugs intermitentes, Claude puede proponer una causa que encaja con el trace pero que no es la tuya. Haz que ordene los candidatos y te diga cómo distinguirlos.
El debugger sigue siendo el dueño del estado. Claude es dueño del significado. Cuando necesitas saber el valor real de una variable en el momento del crash, eso es una ventana de inspección y un breakpoint, no un prompt de chat.
Cuándo Recurrir a Claude — y Cuándo No
Recurre a él cuando:
- El tipo o el mensaje de la excepción te resulta desconocido, o el trace es casi todo internos de framework.
- La causa real está enterrada en inner exceptions o en un
AggregateException. - Estás mirando frames asíncronos y no logras saber cuál de tus líneas es el origen.
- Quieres el arreglo idiomático y sus intercambios, no solo un parche que quite el rojo.
No te molestes cuando:
- Ya sabes la causa — un
NullReferenceExceptionen una línea que acabas de escribir no necesita un LLM. - Necesitas el valor en runtime de algo — usa el debugger.
- El trace contiene secretos o datos de clientes. Las cadenas de conexión, los tokens y los datos personales aparecen en mensajes de excepción y payloads más seguido de lo que te gustaría. Límpialos antes de pegar, especialmente con un modelo en la nube.
La regla de oro: si de otro modo abrirías el navegador para buscar la excepción, mejor pregúntale a Claude — está en el IDE, puede leer tu código, y no te mandará a un hilo de foro muerto de 2015.
Puntos Clave
- El trace muestra maquinaria, no intención — máquinas de estados asíncronas, inner exceptions y frames de framework entierran la única línea que es tuya. Leer traces es reconocimiento de patrones, que es justo donde ayuda un LLM.
- Claude vive en Visual Studio de dos formas — elígelo en el selector de modelo de Copilot Chat para diagnóstico rápido y conversacional; corre Claude Code en la terminal integrada cuando el arreglo es agéntico y abarca varios archivos.
-
El contexto lo es todo — pega el
ex.ToString()completo, apunta al primer frame que es tuyo y describe cuándo pasa. Un mensaje titular recibe una conjetura titular. -
El número de línea suele ser una pista falsa — el ejemplo de EF Core lanza en
ToListAsync, pero el bug es compartir unDbContextentre consultas paralelas. El valor está en la explicación, no en la ubicación. - Verifica, luego confía — reproduce el arreglo, pregunta por qué funciona y nunca pegues secretos. El debugger es dueño del estado; Claude, del significado.



Top comments (0)