Diseño para conservar registros y volcados de memoria cuando falla una aplicación de Windows
· Actualizado el: · Go Komura · Desarrollo en Windows, Manejo de excepciones, Registros, WER, Volcado de memoria en fallos, Investigación de fallos
Lo más doloroso al investigar fallos en una aplicación de Windows es la situación en la que solo se sabe que se cayó, pero no queda ningún rastro de por qué.
Este problema se vuelve especialmente grave en proyectos como los siguientes.
- Solo falla en el entorno del cliente
- Solo falla tras un funcionamiento prolongado
- Aplicaciones WPF, WinForms, servicios de Windows o programas residentes con una tasa de reproducción baja
- Cuando intervienen COM, P/Invoke, DLL nativas o SDK de terceros
- Solo se captura “el mensaje de la excepción”, sin ningún contexto previo
Ahora bien, para ser honestos desde el principio: no es posible garantizar “siempre” un registro solo desde el proceso que se está cayendo. Si se incluyen la corrupción de la pila, la corrupción de memoria, el fast fail, la terminación forzada e incluso el corte de energía, el último registro in-process es, por naturaleza, best effort.
Lo que conviene buscar en la práctica es un diseño que no dependa únicamente de lo que ocurre dentro del proceso que se cae. Es decir,
- Registro cronológico en condiciones normales
- El marcador final de fallo en el instante en que el proceso se cae
- El rastro del fallo que deja el sistema operativo o un proceso separado
Se piensa en estas tres capas.
En este artículo, partiendo de aplicaciones de escritorio de Windows, programas residentes, servicios de Windows y herramientas de integración con equipos, se organizan las mejores prácticas para no perder la capacidad de investigación aunque la aplicación se caiga por una excepción originada en un error de programación.
1. Conclusión por adelantado
Primero se enumeran solo las conclusiones.
- No apostar “el último registro” a un único manejador in-process es lo más importante.
- En la práctica, la combinación más segura es registro habitual + marcador final de fallo + WER LocalDumps.
- Si hay funcionamiento prolongado, integración con equipos, complementos o una mezcla de SDK nativos, sumar un proceso de vigilancia (watchdog / launcher / servicio) refuerza mucho el diseño.
- En el manejador de fallos, la regla de oro es no realizar procesamiento pesado. Se excluyen la compresión, el envío por HTTP, la resolución de DI, los cuadros de diálogo de interfaz y la generación de JSON complejo.
- En el momento del fallo, basta con dejar un rastro breve en local; la compresión, la subida y la notificación se dejan para el siguiente arranque o para otro proceso.
- Diseñar para “prolongar la vida” en apariencia usando ThreadException de WinForms o DispatcherUnhandledException de WPF es peligroso frente a errores de programación.
- Tanto en .NET como en código nativo, ante excepciones que hagan sospechar un estado dañado, es más seguro basarse en “registrar y terminar” antes que en “recuperar”.
- Si se van a capturar volcados de memoria, hay que conservar al mismo tiempo los PDB y los binarios distribuidos, o luego no se podrán interpretar.
En resumen, la mejor práctica es “No intentar hacerlo todo en el instante del fallo. Repartir responsabilidades entre antes de caer, en el instante de caer y después de caer.”
1.1 Terminología usada en este artículo
A continuación se enumeran, por adelantado, los términos que se usarán sin más aclaración en el resto del artículo.
| Término | Desarrollo / lectura | Significado |
|---|---|---|
| WER | Windows Error Reporting, informe de errores de Windows | Mecanismo de Windows que captura y registra, del lado del sistema operativo, la terminación anómala de una aplicación. La configuración que deja el volcado en local es LocalDumps |
| Volcado de memoria / minidump | crash dump | Archivo que guarda el contenido de memoria del proceso en el instante en que se cayó. Permite examinar después los hilos, la pila y los módulos |
| PDB | Program Database | Archivo de símbolos que se genera durante la compilación. Sin él, aunque se abra el volcado, no aparecen los nombres de función ni los números de línea |
| in-process | dentro del proceso | Procesar algo dentro del propio proceso que se está cayendo. Lo contrario es un proceso separado |
| best effort | mejor esfuerzo | Propiedad de “si sale bien, queda; no hay garantía”. El registro in-process en el instante del fallo tiene esta naturaleza |
fast fail / __fastfail |
terminación de alta velocidad por fallo | Mecanismo que, al determinar que el estado está dañado, termina de inmediato con el mínimo de pasos y sin limpieza. En código nativo equivale a __fastfail, y en .NET a Environment.FailFast |
| watchdog | proceso de vigilancia | Proceso separado que vigila desde fuera el inicio, la finalización y la supervivencia del proceso principal. También puede implementarse como un launcher o un servicio padre |
| heartbeat | señal de vida | Aviso periódico que indica al watchdog “todavía estoy funcionando” |
| Ruta UNC | Universal Naming Convention | Ruta de recurso compartido de red con el formato \\servidor\recurso\.... Usarla en el momento del fallo puede provocar esperas por cortes momentáneos o por credenciales |
| ACL | Access Control List, lista de control de acceso | Configuración de quién puede leer y escribir en una carpeta determinada. Es una causa habitual de que los volcados o registros “no se generen” |
| SEH | Structured Exception Handling, manejo estructurado de excepciones | Mecanismo nativo de excepciones de Windows. SetUnhandledExceptionFilter pertenece a este grupo |
| CRT | C Runtime, tiempo de ejecución de C | Implementación de la biblioteca estándar de C/C++. Tiene sus propias rutas de terminación, distintas de las de SEH |
| session | ID de sesión | Valor que identifica “de qué instancia de arranque se trata”. Es la clave para cruzar los registros, los volcados y los registros del watchdog |
2. Por qué no se puede garantizar la fiabilidad solo con lo que ocurre in-process
Si este punto queda ambiguo, el diseño se tambalea.
2.1 El propio contexto del hilo que se cayó puede estar dañado
Los ganchos de excepción no controlada y los filtros de excepción de nivel superior a veces se ejecutan en el contexto del hilo que ya está dañado. En ese momento,
- la pila ya está en riesgo
- una asignación adicional es arriesgada por la corrupción del heap
- esperar puede bloquear el proceso por un bloqueo (lock) que ya estaba tomado cuando ocurrió la excepción
- el objeto del que depende el propio logger ya está dañado
son situaciones que ocurren con normalidad.
Por eso es más seguro considerar el manejador final no como “un lugar donde se puede hacer cualquier cosa”, sino como “un lugar donde se puede hacer muy poco”.
2.2 El fast fail y las excepciones por estado dañado parten de un “funcionamiento in-process mínimo”
Ante la corrupción de memoria o un estado fatal, es mejor no esperar nada del manejo de excepciones habitual.
En particular, la familia __fastfail del lado nativo y las anomalías que hacen sospechar un estado dañado están diseñadas para “terminar de inmediato con la menor sobrecarga posible”.
Es decir, resulta natural pensar que si se logra escribir el último registro in-process, es un golpe de suerte, y el rastro principal debe recaer en el sistema operativo o en un proceso separado.
2.3 El evento de excepción no controlada de .NET tampoco es el lugar para una “recuperación pesada”
AppDomain.UnhandledException de .NET es útil, pero es mejor pensar que lo único permitido aquí es un registro breve.
- Puede verse afectado por un bloqueo que ya se mantenía cuando ocurrió la excepción
- No todas las excepciones, incluidas las de estado dañado, se pueden capturar de forma segura
- Si aquí se fuerza una política de continuación, es fácil terminar prolongando la vida en un estado a medio romper
Lo realista es entender que “el evento de excepción no controlada equivale al último aviso”, y no “un punto de recuperación seguro”.
3. Arquitectura recomendada: separar el momento del fallo del momento posterior al reinicio
La forma más fácil de organizar esto es separar lo que se hace en el momento del fallo de lo que se hace después de reiniciar.
Antes de nada, resumimos en un solo diagrama de qué proceso es responsable de cada una de las tres capas y dónde termina cada rastro.
flowchart TD
accTitle: Reparto de responsabilidades entre el registro habitual, el marcador final de fallo, WER LocalDumps y el proceso de vigilancia
accDescr: Diagrama que muestra cómo el registro habitual y el marcador final de fallo se escriben dentro del proceso de la aplicación, cómo WER LocalDumps guarda el volcado de memoria fuera de ese proceso, cómo el proceso watchdog registra el código de salida y la hora de finalización, y cómo el siguiente proceso saludable que arranca se encarga de comprimir, subir y notificar a partir de esos datos en una carpeta local fija
subgraph APP["Proceso de la aplicación"]
L1["Registro habitual<br/>serie temporal append-only"]
L2["Marcador final de fallo<br/>escribe 1 línea y termina"]
end
subgraph WIN["Lado de Windows"]
WER["WER LocalDumps<br/>guarda el volcado fuera del proceso"]
end
subgraph WD["Proceso watchdog"]
EX["Registra el código de salida y la hora de finalización<br/>decide si reiniciar"]
end
subgraph NEXT["Siguiente proceso saludable que arranca"]
POST["Compresión / subida / notificación<br/>detección de la finalización anómala anterior"]
end
DISK[("Carpeta local fija")]
L1 --> DISK
L2 --> DISK
WER --> DISK
EX --> DISK
DISK --> POST
L1 -. Ocurre una excepción .-> L2
L2 -. Termina el proceso .-> WER
L2 -. Termina el proceso .-> EX
Hay tres puntos clave.
- Lo único que escribe por sí mismo el proceso que se cae son los dos elementos dentro del recuadro “Proceso de la aplicación”. Y, además, el marcador final de fallo se limita a “escribir una línea y terminar”.
- El rastro principal está fuera del proceso. El volcado de WER y el registro del watchdog quedan aunque la aplicación esté dañada.
- La clave para cruzar la información es un session ID y un PID comunes. Si estos no coinciden, los tres rastros parecen pertenecer a incidentes distintos.
| Fase | Objetivo | Dónde se ejecuta | Qué hacer |
|---|---|---|---|
| Funcionamiento normal | Dejar la línea de tiempo | Dentro de la aplicación | Registro estructurado, heartbeat, eventos de frontera |
| Momento del fallo | Dejar el rastro mínimo | Dentro de la aplicación + SO | Marcador final de fallo, volcado de WER |
| Justo después de terminar | Detectar una salida inesperada | Proceso separado | Registro del código de salida, decisión de reinicio, notificación |
| Después del siguiente arranque | Realizar el posprocesamiento pesado | Proceso nuevo y saludable | Compresión, subida, notificación al usuario, limpieza de registros antiguos |
Con esta división, el diseño se vuelve bastante estable.
3.1 Configuración mínima
Para una herramienta de negocio pequeña o una aplicación WPF/WinForms de uso interno, suele bastar con lo siguiente para empezar.
- Registro habitual: archivo local append-only
- Marcador final de fallo: archivo corto dedicado
- Volcado de memoria: WER LocalDumps
- Al siguiente arranque: mostrar “La vez anterior la aplicación terminó de forma anómala. Hay información de diagnóstico disponible”
3.2 Configuración reforzada
Conviene reforzar un nivel cuando existen requisitos como estos.
- Funcionamiento 24/7
- Control de equipos, monitoreo, funcionamiento residente
- Uso intensivo de COM, P/Invoke o SDK nativos
- Existen procesos hijos, complementos o ejecución de scripts
- No se puede tolerar que en el entorno del cliente la aplicación “quede detenida”
En ese caso,
- Proceso worker: el procesamiento principal
- launcher / watchdog / servicio: vigilancia del arranque, registro de la salida, reinicio
- WER LocalDumps: del lado del worker
- Siguiente arranque o watchdog: recolección de información de diagnóstico
Con esta división, el diseño resulta bastante más práctico.
4. Buenas prácticas para el registro habitual
Si se intenta resolver todo con la última línea del momento del fallo, casi siempre se pierde.
Lo que de verdad funciona es el registro habitual hasta justo antes del fallo.
4.1 El registro debe ser “información que se pueda correlacionar después”, más que “un texto pensado para personas”
Se enumeran los campos mínimos que conviene incluir en el registro habitual.
- Marca de tiempo en UTC
- Tiempo transcurrido desde el inicio del proceso
- PID / TID
- Nombre de la aplicación, versión, número de compilación, identificador del commit
- ID de sesión
- ID de operación / ID de trabajo / ID de correlación
- Nombre del módulo / de la pantalla / del worker
- El efecto externo inmediatamente anterior
- Escritura de archivo
- Actualización de base de datos
- Envío de comando al equipo
- Solicitud de comunicación
- Tipo de excepción, HRESULT / error de Win32 / código de excepción
- Resumen de los parámetros de entrada principales
- ID del objeto afectado, siempre que no incluya información confidencial
Lo recomendable es un formato JSON Lines o key=value con un evento por línea.
Con JSON Lines, un evento queda con este nivel de detalle.
{"ts":"2026-03-18T01:15:33.412Z","up_ms":184213,"pid":1234,"tid":9,"app":"MyApp","ver":"3.2.1.884","commit":"9f1c2ab","session":"4f1c","level":"Warning","module":"DeviceWorker","op":"JOB-20260318-0042","event":"ExternalCommandSent","target":"COM3","cmd":"SEQ_START","timeout_ms":3000}
Es largo, pero con ver solo esta línea se sabe cuándo, en qué instancia de arranque, con qué versión, en medio de qué operación y qué se hizo. Se conecta con el volcado de memoria a través de pid y session, y con la compilación a través de ver y commit.
Si se usa el formato key=value, el mismo contenido queda así.
ts=2026-03-18T01:15:33.412Z up_ms=184213 pid=1234 tid=9 app=MyApp ver=3.2.1.884 commit=9f1c2ab session=4f1c level=Warning module=DeviceWorker op=JOB-20260318-0042 event=ExternalCommandSent target=COM3 cmd=SEQ_START timeout_ms=3000
Más que dejar un texto largo pensado para personas, lo importante es “poder cruzar después los tres archivos”.
4.2 Los eventos críticos se dejan de forma síncrona
Si todo el registro habitual se escribe de forma síncrona, se vuelve pesado.
Pero si todo se deja en manos de un búfer asíncrono, en el instante del fallo puede desaparecer todo de golpe.
Por eso, en la práctica es realista variar el tratamiento según el nivel.
- Eventos detallados de nivel
Information: se pueden dejar en búfer - Nivel
Warningo superior: hacer flush cuanto antes - Eventos de frontera importantes: dejarlos de forma síncrona
- ProcessStart
- ConfigLoaded
- WorkerStarted
- ExternalCommandSent
- TransactionCommitted
- RecoveryStarted
- FatalPathEntered
En resumen, se trata de asegurar que al menos los límites relevantes del negocio queden grabados en firme.
4.3 Separar “el registro habitual que se está escribiendo ahora” de “el marcador final de fallo”
Esto es bastante importante.
Si se intenta meter todo en un único rolling log,
- estaba en medio de una rotación
- seguía pendiente en la cola asíncrona
- el propio logger murió justo después de ocurrir la excepción
- la línea de registro quedó cortada a la mitad
son cosas que pueden pasar.
Por eso se recomienda separarlo en al menos dos archivos.
app-<session>.jsonlregistro cronológico habitualfatal-last.logofatal-<session>.logdedicado exclusivamente al marcador final de fallo
Con solo tener claro “dónde queda la última línea”, se gana muchísimo en el terreno.
4.4 El destino del registro debe ser una ruta local fija; no usar destinos de red
Depender de una ruta UNC, un NAS, HTTP o una API en la nube en el momento del fallo es peligroso.
- Cortes momentáneos de red
- Retrasos de DNS
- Expiración de credenciales
- Espera en el hilo de la interfaz
- Permisos insuficientes de la cuenta de servicio
son factores que pueden intervenir.
En el momento del fallo, primero se deja en una ruta local fija.
El envío se hace después del siguiente arranque o desde otro proceso.
4.5 Incluir el session en el nombre de archivo
Con la fecha sola no basta.
Porque un mismo día se puede reiniciar varias veces.
Por ejemplo, así.
Logs\
MyApp_20260318_101530_pid1234_session-4f1c.jsonl
MyApp_fatal_20260318_101533_pid1234_session-4f1c.log
MyApp_watchdog_20260318.jsonl
Con solo dejar claro “de qué instancia de arranque se trata”, la velocidad del análisis cambia mucho.
5. Buenas prácticas para el marcador final de fallo
Este no es el lugar para construir un logger con todas las funciones. Es el lugar donde se deja una sola vez, algo breve y lo más confiable posible.
5.1 El objetivo no son “los detalles de la causa”, sino “fijar el punto de entrada”
Conviene limitar la información que se incluye en el marcador final de fallo.
- Hora UTC en que ocurrió
- PID / TID
- ID de sesión
- Versión / número de compilación
- Desde qué gancho llegó
AppDomain.UnhandledExceptionApplication.ThreadExceptionDispatcherUnhandledExceptionSetUnhandledExceptionFilter_set_invalid_parameter_handlerset_terminate
- Tipo de excepción o código de excepción
- Un mensaje breve, si es posible
- ID de la operación inmediatamente anterior
- Nombre del archivo del registro habitual
- Carpeta prevista para el volcado de memoria
Con esto basta.
5.2 Qué no hacer dentro del manejador de fallos
Todo esto tiene una probabilidad bastante alta de convertirse en una mina.
- Resolver el logger desde un contenedor de DI
- Usar async/await
- Lanzar un Task
- Esperar un bloqueo
- Construir JSON complejo
- Manipular objetos COM
- Mostrar cuadros de diálogo de la interfaz
- Comprimir
- Enviar por HTTP / SMTP / Slack / Teams
- Analizar el volcado de memoria y resumirlo
- Atrapar la excepción y continuar
El manejador de fallos no es la continuación del flujo normal de procesamiento. Debe limitarse a “escribir lo mínimo en local y terminar”.
5.3 Qué hacer dentro del manejador de fallos
En cambio, lo que hay que hacer es bastante simple.
- Evitar la reentrada
- Escribir una sola línea
- Hacer flush
- Terminar
En este orden.
Si es posible, usar
- una carpeta dedicada creada de antemano
- una ruta cuya existencia ya se verificó de antemano
- un destino cuya ACL ya se verificó
En el registro habitual, hacer flush en exceso resulta pesado, pero el marcador de fallo tiene un volumen mínimo de escrituras, así que aquí sí se puede hacer flush de forma más agresiva.
Diseñar tratando esta única línea como algo que hay que “grabar en firme de inmediato” —con FileStream.Flush(true) en .NET, o con FlushFileBuffers en código nativo— facilita el diseño.
5.4 No intentar continuar
Ante una excepción inesperada originada por un error de programación, es más seguro pensar en el manejador final como un dispositivo de registro, no de recuperación.
Los siguientes son los casos en los que particularmente conviene tomar “no continuar” como norma.
- Aunque sea un
NullReferenceExceptiono unInvalidOperationException, si ocurrió en medio de la actualización de un estado compartido - Una excepción inesperada en el hilo de la interfaz
- Una excepción inesperada que se escapó de un bucle de monitoreo o de un bucle padre
AccessViolationExceptionStackOverflowException- Una anomalía en el límite con código nativo
- invalid parameter / purecall / terminate del CRT
Es comprensible el deseo de “no querer que se caiga”, pero sobrevivir a medio romper suele ser más difícil, tanto para el diagnóstico como para la operación.
Al terminar el proceso, conviene evaluar una API de terminación inmediata: Environment.FailFast en .NET, o RaiseFailFastException o __fastfail en código nativo, y diseñar sin esperar nada de finally ni de la limpieza habitual.
5.5 Implementación mínima en C#
Si se traslada directamente a C# sobre .NET 6 o posterior la política descrita hasta aquí, el resultado tiene aproximadamente esta extensión. No se usa ni DI ni logger.
using System;
using System.IO;
using System.Text;
using System.Threading;
internal static class FatalMarker
{
// Ruta local fija que se crea de antemano y para la que ya se probó la escritura
private static readonly string LogDirectory = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"MyApp", "Logs");
// Clave para cruzar el registro habitual, el volcado de memoria y el registro del watchdog
private static readonly string SessionId = Guid.NewGuid().ToString("N").Substring(0, 8);
// Bandera para evitar la reentrada. 0 significa que aún no se escribió
private static int _written;
/// <summary>Se llama una sola vez, justo después de iniciar la aplicación.</summary>
public static void Install()
{
// Se crea aquí para no tener que llamar a CreateDirectory en el momento del fallo
Directory.CreateDirectory(LogDirectory);
AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
}
/// <summary>Se llama desde el punto donde se decide que "no se puede continuar".</summary>
public static void FailNow(string reason)
{
Write("FailNow", null, reason);
Environment.FailFast(reason);
}
private static void OnUnhandledException(object sender, UnhandledExceptionEventArgs e)
{
Write("AppDomain.UnhandledException", e.ExceptionObject as Exception, null);
// Aquí no se termina el proceso. Como es una excepción no controlada, el CLR continuará con su procesamiento de finalización predeterminado.
// Si se llama a FailFast aquí, la causa que queda registrada en el volcado de WER
// se sustituye por FailFast en lugar de la excepción original.
}
private static void Write(string hook, Exception ex, string note)
{
// A partir de la segunda vez no hace nada
if (Interlocked.Exchange(ref _written, 1) != 0)
{
return;
}
try
{
string fileName = string.Format(
"MyApp_fatal_{0:yyyyMMdd_HHmmss}_pid{1}_session-{2}.log",
DateTime.UtcNow, Environment.ProcessId, SessionId);
string path = Path.Combine(LogDirectory, fileName);
// Arma la línea solo con concatenación de cadenas, sin llamar a ningún serializador
string line = string.Join("\t",
"ts=" + DateTime.UtcNow.ToString("O"),
"pid=" + Environment.ProcessId,
"tid=" + Environment.CurrentManagedThreadId,
"session=" + SessionId,
"hook=" + hook,
"type=" + (ex != null ? ex.GetType().FullName : "none"),
"message=" + Flatten(ex != null ? ex.Message : note),
"log=" + LogDirectory);
using (var stream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
{
byte[] bytes = new UTF8Encoding(false).GetBytes(line + Environment.NewLine);
stream.Write(bytes, 0, bytes.Length);
// Al pasar true, los datos se envían hasta el disco, no solo a la caché del SO
stream.Flush(true);
}
}
catch
{
// Si esto falla, ya no hay nada más que hacer. Se atrapa la excepción y termina
}
}
private static string Flatten(string s)
{
return string.IsNullOrEmpty(s) ? string.Empty : s.Replace("\r", " ").Replace("\n", " ");
}
}
Del lado de quien lo invoca, basta con llamar a Install() justo después de arrancar. Si se olvida este paso, el código anterior no tiene ningún efecto.
internal static class Program
{
private static void Main(string[] args)
{
FatalMarker.Install();
// A partir de aquí, el proceso normal de inicio de la aplicación
RunApplication(args);
}
private static void RunApplication(string[] args)
{
// Ejemplo: si se determina que el estado compartido está dañado, se termina sin continuar
// FatalMarker.FailNow("device state inconsistent");
}
}
Estas 60 líneas solo respetan los cuatro puntos mencionados en 5.3.
- Con
Interlocked.Exchangese evita la reentrada - Con solo concatenación de cadenas se escribe una línea
- Con
Flush(true)se graba hasta el disco - Desde
UnhandledExceptionno se permite continuar
Environment.FailFast escribe el mensaje en el Visor de eventos de aplicación de Windows antes de terminar el proceso de inmediato, e incluye ese contenido también en el informe de errores. Es decir, el propio FailFast añade un rastro más. Sin embargo, como se mencionó antes, llamarlo desde la ruta de una excepción no controlada cambia cómo se ve el volcado de memoria, así que hay que elegir bien desde dónde se llama.
6. Consideraciones según el framework
6.1 Común a .NET: AppDomain.CurrentDomain.UnhandledException
Es útil como el último aviso.
Sin embargo, hay que evitar aquí cualquier procesamiento de recuperación pesado.
El uso básico es simple.
- Escribir el marcador final de fallo
- Si hace falta, dejar un mensaje mínimo en el Visor de eventos de Windows
- No continuar
- No esperar ni reintentar aquí
UnhandledException es útil, pero es más seguro no partir de la idea de que aquí se puede devolver la aplicación a un estado saludable.
6.2 WinForms: Application.ThreadException
Lo complicado aquí es que captura la excepción no controlada del hilo de interfaz y permite continuar en apariencia.
Para convertir en un cuadro de diálogo un error esperado de entrada de datos del negocio puede valer, pero no es adecuado para continuar ante una excepción inesperada originada por un error de programación.
Si de verdad se prioriza la investigación de la causa,
- hacer solo un registro mínimo en
ThreadException - o bien optar por
UnhandledExceptionMode.ThrowException - y, sobre esa base, terminar el proceso dejando el volcado de memoria y el registro
es más seguro.
6.3 WPF: Application.DispatcherUnhandledException
En WPF ocurre algo similar.
- El objetivo principal es solo la excepción en el hilo de interfaz
- Con
Handled = truese puede continuar en apariencia - Pero si se hace eso frente a un error de programación, es fácil que el estado de la pantalla y el estado interno se desincronicen
Por eso, también en WPF es más prudente usarlo como entrada de registro y no como un dispositivo para prolongar la ejecución.
6.4 No convertir TaskScheduler.UnobservedTaskException en la vía principal
Esto no es “el último bastión justo antes de caer”.
Se puede usar como apoyo para detectar excepciones de Task que se escaparon sin observarse, pero
es débil como vía confiable de registro en el momento del fallo.
Por eso,
- detectar tempranamente excepciones que no se observaron
- sacar a la luz, durante el desarrollo, fallos de diseño en el uso de
Task
puede usarse para estos fines, pero es mejor no convertirlo en el protagonista del manejador final de fallos.
6.5 Nativo Win32/C++: no confiar en exceso en SetUnhandledExceptionFilter
Del lado nativo, es tentador depositar demasiada confianza en SetUnhandledExceptionFilter.
Sin embargo, como se ejecuta en el contexto del hilo que falló,
- una pila inválida
- una recursión profunda
- un heap ya dañado
- un bloqueo retenido en el momento de la excepción
pueden afectarlo.
Por lo tanto, lo más adecuado es considerar SetUnhandledExceptionFilter
como una entrada best effort para recibir el último aviso.
6.6 En C++ nativo también hay que cubrir las rutas de terminación del CRT
En C++ nativo, si solo se vigila el SEH no controlado, quedan huecos.
En concreto, conviene vigilar lo siguiente.
_set_invalid_parameter_handler_set_purecall_handlerset_terminate
Esta familia sirve para cubrir las “rutas de terminación” originadas en el tiempo de ejecución de C o de C++.
En la práctica,
- escribir el marcador final de fallo también desde estos manejadores
- pero sin realizar procesamiento de recuperación pesado
- terminar el proceso de forma segura
- dejar el rastro principal en manos de WER y del volcado de memoria
es lo prudente.
7. Usar WER LocalDumps como base
Este punto resulta bastante sólido en la práctica.
7.1 Lo primero que se recomienda es WER LocalDumps
En el sentido de “dejar, después del fallo, el rastro mínimo de la forma más confiable posible”, lo primero y más manejable es WER LocalDumps.
La razón es simple.
- El volcado se puede dejar del lado del sistema operativo
- Es fácil de habilitar sin herramientas adicionales
- Se puede configurar por aplicación
- Permite sacar el rastro principal del momento del fallo fuera del proceso
Lo que no se puede saber solo con el registro
- qué hilo se cayó
- en qué pila se cayó
- en qué frontera de módulo ocurrió
- qué componente resulta sospechoso: managed, nativo, COM o el SDK
es lo que se puede examinar después, y ahí está su fuerza.
7.2 Configuración típica
Por ejemplo, para dejar los volcados de MyApp.exe en C:\CrashDumps\MyApp, se puede hacer así.
reg add "HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\MyApp.exe" /f
reg add "HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\MyApp.exe" /v DumpFolder /t REG_EXPAND_SZ /d "C:\CrashDumps\MyApp" /f
reg add "HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\MyApp.exe" /v DumpCount /t REG_DWORD /d 10 /f
reg add "HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\MyApp.exe" /v DumpType /t REG_DWORD /d 2 /f
Al principio basta con simplificar hasta este punto.
| Valor | Recomendación inicial |
|---|---|
DumpFolder |
Una carpeta dedicada |
DumpCount |
Entre 5 y 10 |
DumpType |
2 en máquinas de desarrollo; 1 o 2 en campo, según el espacio y los requisitos de confidencialidad |
7.3 Verificar siempre la ACL del destino del volcado de memoria
Tanto para el registro como para el volcado de memoria ocurre lo mismo: configurar una carpeta en la que no se puede escribir no sirve de nada.
En particular,
- Servicios de Windows
- Procesos hijos con permisos separados
- Cuentas restringidas en las máquinas de campo
- Situaciones que involucran UAC
son casos en los que la ACL del destino es la principal causa de que la captura quede en blanco.
Del destino conviene verificar,
- creación previa
- una prueba de escritura
- el límite de retención
- si es un lugar al que puede acceder el responsable de operación
7.4 Cuando se quiere adjuntar el registro actual al informe de WER
Si se usa el informe de WER hacia Microsoft o una operación propia basada en WER, también existe la opción de usar WerRegisterFile para registrar el archivo de registro actual de modo que se incluya en el informe de errores.
Sin embargo, es más seguro considerar esto una vía adicional, no un sustituto del guardado local. Porque lo que de verdad se necesita en el momento del fallo es, ante todo, que quede algo de la forma más confiable posible en el propio equipo.
En cuanto al orden,
- Registro habitual local
- Marcador de fallo local
- Volcado de memoria local
- Si hace falta, registrar también los archivos relacionados en la vía de envío de WER
es más práctico.
7.5 Conservar el control de versiones, no solo el volcado de memoria
Aunque se capture el volcado de memoria, si luego
- no está el EXE/DLL de ese momento
- no está el PDB
- no se sabe de qué commit era la compilación
el volcado pierde gran parte de su valor.
Como mínimo, hay que conservar lo siguiente.
- El binario distribuido
- El PDB correspondiente
- La versión
- La fecha y hora de compilación
- El identificador del commit
- La versión del instalador
La recolección del volcado de memoria y la conservación del PDB van siempre juntas.
7.6 Los primeros pasos al abrir un volcado de memoria ya capturado
Es muy habitual la situación de “hay volcados acumulados, pero nadie los ha abierto nunca”. Dejando el análisis profundo para un artículo especializado, aquí se deja solo lo necesario para los primeros diez minutos.
Los preparativos son dos.
- Reunir en una sola carpeta el PDB y el binario distribuido de la misma compilación que ese volcado
- Preparar WinDbg, incluido en las Debugging Tools for Windows del Windows SDK
Después, se abre el .dmp en WinDbg y se escriben estos comandos en orden.
.sympath srv*C:\symbols*https://msdl.microsoft.com/download/symbols;C:\myapp\pdb
.reload /f
.ecxr
!analyze -v
~*k
lm v
El papel de cada uno es el siguiente.
| Comando | Qué hace |
|---|---|
.sympath |
Configura las rutas de búsqueda de símbolos. Se indican tanto el servidor de símbolos de Microsoft como la ubicación de los propios PDB |
.reload /f |
Fuerza la recarga de los símbolos |
.ecxr |
Se mueve al registro de contexto en el momento en que ocurrió la excepción. Si se olvida este paso, se termina viendo la pila de espera del lado de WER en lugar del punto donde ocurrió el fallo |
!analyze -v |
Analiza automáticamente la excepción actual y muestra el detalle. Conviene leer esto primero |
~*k |
Muestra la pila de llamadas de todos los hilos. Permite ver qué hacían los demás hilos además del que se cayó |
lm v |
Muestra los módulos cargados y su versión. Sirve para cruzar el binario distribuido con el número de compilación |
Si al llegar hasta aquí “no aparece el nombre de la función” o “no aparece el número de línea”, la causa suele ser que el PDB es de una compilación distinta. Vuelva al control de versiones de 7.5.
Los temas de recolección y análisis más avanzados están reunidos en los artículos relacionados al final.
8. Consideraciones al usar MiniDumpWriteDump o un reportero de fallos propio
También hay situaciones en las que hace falta una implementación propia.
- Se quiere ofrecer un botón de “Guardar información de diagnóstico” en la interfaz
- Se quiere agrupar también los registros y los archivos de configuración
- Se quiere manejar en conjunto un grupo de procesos hijos
- Se quiere aplicar un enmascarado propio antes de la subida automática
Sin embargo, lo más importante aquí es no cargar en exceso al lado que se está cayendo con el propio procesamiento de captura del volcado.
8.1 Un proceso separado, mejor que el self-dump
MiniDumpWriteDump es potente, pero
es más seguro llamarlo desde un proceso separado que desde dentro del propio proceso que falló.
Una configuración típica sería esta.
- El worker principal detecta la anomalía
- Si es posible, notifica al helper mediante un evento o una tubería con nombre
- El helper captura el volcado de memoria del worker
- El helper agrupa el
taildel registro y los archivos de configuración - El helper coloca todo en la cola de subida tras terminar
Así, aunque el worker esté dañado, el lado del helper sigue sano.
8.2 Si no queda más remedio que hacerlo in-process, delegarlo a un hilo dedicado
Incluso cuando no es posible separarlo en otro proceso, es mejor reservar un hilo dedicado exclusivamente al volcado de memoria.
Aun así, en esencia sigue siendo best effort. No se vuelve “100% seguro solo porque se incorporó una implementación propia de volcado”.
8.3 Dejar lo pesado para después del siguiente arranque
Hay cosas que resulta tentador hacer en un reportero propio.
- Compresión en zip
- Cruce con la información de símbolos
- Subida al servidor
- Captura de pantalla
- Obtención de información adicional desde la base de datos
Todo esto se deja no para el momento del fallo, sino para después del reinicio o del lado del helper.
9. Qué cambia al incorporar un proceso de vigilancia
En sistemas de funcionamiento prolongado, el proceso de vigilancia resulta bastante eficaz.
9.1 Lo que deja el proceso de vigilancia
Del lado del watchdog, el launcher o el servicio padre se puede dejar información como la siguiente.
- Hora de inicio del proceso hijo
- Argumentos de arranque
- PID
- Versión del objetivo vigilado
- Hora de la última señal de vida recibida
- Hora de finalización
- Código de salida
- Número de reinicios
- Si hay o no volcado de memoria
- Si se reinició o no
Con solo tener esto,
- si realmente se cayó
- si fue un apagado del sistema operativo
- si el usuario lo cerró
- si se lo mató (kill) tras quedar colgado
- cuántas veces entró en un bucle de reinicio
queda bastante claro.
9.2 Casos especialmente adecuados
Conviene considerar seriamente la separación en casos como estos.
- Un worker que integra un SDK de terceros
- Procesamiento de imágenes, de video o E/S de dispositivos
- Un bucle padre de monitoreo o sondeo (polling)
- Ejecución de scripts o complementos
- Alojamiento de activos existentes de COM/ActiveX
- Puentes o interoperabilidad entre 64 bits y 32 bits
Confinar el procesamiento peligroso en un único worker facilita tanto el diseño del registro como el de la recuperación.
10. Errores habituales
Aquí se reúnen trampas a nivel de diseño. El tema a nivel de procedimiento, “qué no hacer dentro del manejador de fallos”, ya está en 5.2, así que quienes estén implementando deberían consultar esa sección. Los puntos que parecen solaparse (como el envío por HTTP de 10.3) tratan cosas distintas: 5.2 explica “por qué es un problema hacerlo dentro del manejador”, y esta sección explica “qué termina pasando en la operación como consecuencia”.
10.1 Atrapar con catch (Exception), dejar solo un registro y continuar
Es lo más habitual y lo más peligroso.
- Quedan cambios a medio aplicar
- Se daña el estado compartido
- Aumentan los fallos posteriores
- Se difumina el punto real de la causa
A cambio de una línea más de registro, el incidente suele alargarse.
10.2 Confiar únicamente en la cola de un logger asíncrono
El registro asíncrono en sí mismo no es malo.
El problema es que incluso en la ruta fatal se termina apilando en la misma cola.
Si el worker se detiene en el instante del fallo, esa cola desaparece por completo.
Es más seguro tener una vía de escape en la que solo la ruta fatal escriba directamente.
10.3 Enviar por HTTP desde el manejador de fallos
Es tentador implementarlo, pero resulta bastante peligroso.
- DNS
- TLS
- proxy
- Autenticación
- Tiempo de espera agotado
- Espera de reenvío
Todo esto queda montado sobre el contexto ya dañado.
El envío se hace después del reinicio.
10.4 Hay volcado de memoria, pero no se puede vincular con el registro habitual
Esto es frecuente.
- El nombre del archivo de volcado no incluye el session
- El registro no incluye el PID ni el session
- El watchdog no incluye el PID
- El número de compilación no coincide
Como resultado, los tres rastros terminan pareciendo historias distintas.
10.5 Prolongar la vida con el evento de excepción no controlada de WinForms o WPF
Como en apariencia “deja de caerse”, al principio se recibe bien. Pero, en realidad,
- solo queda la pantalla
- el worker ya está muerto
- solo queda el botón activo
- no se sabe si el guardado se completó
y es fácil terminar en este tipo de estado zombi.
10.6 No vigilar las rutas de terminación del lado nativo
Si uno se confía solo con SetUnhandledExceptionFilter,
- invalid parameter
- purecall
- terminate
- fast fail
quedan huecos del lado de estas rutas.
En C++ nativo conviene tener presentes no solo el SEH, sino también las rutas de terminación del lado del CRT y del tiempo de ejecución de C++.
11. Lista de verificación mínima para la implementación
Si se cumple lo siguiente, el diseño resulta bastante sólido para producción.
- El registro habitual queda con un evento por línea
- Todos los registros incluyen UTC, PID, TID, version y session
- Quedan registrados
ProcessStartyProcessExit - Los eventos de frontera importantes se flushean de forma síncrona
- Existe un archivo dedicado al marcador final de fallo
- La ruta fatal no pasa por el logger asíncrono
- WER LocalDumps está configurado por aplicación
- Se verificó la ACL del destino del volcado de memoria
- Se conservan el PDB y el binario distribuido
- El siguiente arranque puede detectar la finalización anómala anterior
- La compresión, la subida y la notificación se realizan después del reinicio o desde otro proceso
- En C++ nativo también se organizaron invalid parameter, purecall y terminate
- Se provocó una caída intencional en una máquina de pruebas y se confirmó si de verdad queda registrado
El último punto es especialmente importante. Diseñarlo no basta: es indispensable hacer una “prueba de que realmente se captura”.
12. Hasta dónde probar
Se resumen en una tabla los puntos que conviene verificar.
| Prueba | Qué verificar |
|---|---|
| Excepción no controlada en código managed | Si quedan completos el registro habitual, el marcador de fallo y el volcado de memoria |
| Excepción en el hilo de interfaz | Si la vía del evento de WinForms/WPF funciona como se esperaba |
| Excepción en un hilo worker | Si llega hasta AppDomain.UnhandledException y si el watchdog puede detectarla |
| Excepción nativa | Si realmente se captura el volcado de memoria de WER |
| invalid parameter / terminate | Si queda un registro mínimo incluso por la vía del CRT/tiempo de ejecución de C++ |
| Kill forzado | Si, aunque sea imposible in-process, el lado del watchdog puede registrar la salida inesperada |
| Reinicio | Si funcionan la notificación, la recolección y la subida tras el siguiente arranque |
Lo importante es verificar, no “si salta una excepción debería salir un registro”, sino “bajo esta condición queda este archivo”.
13. Resumen
Si se quiere conservar la información necesaria para investigar aunque una aplicación de Windows se caiga por una excepción originada en un error de programación, el eje del razonamiento es bastante simple.
- No depender únicamente del proceso que se cae
- Separar en registro habitual, marcador final de fallo y rastro del lado del sistema operativo o de un proceso separado
- En el momento del fallo, limitarse a dejar algo breve en local
- Dejar el procesamiento pesado para después del reinicio o para otro proceso
- Usar WER LocalDumps como base
- Basarse en registrar y terminar, antes que en continuar
Al final, es más sólido “construir un diseño que se pueda seguir aun sin la última línea” que “esforzarse por conseguir esa última línea”.
Aun así, se quiere tener esa última línea, así que el marcador final de fallo se deja, breve, en un archivo separado. Y el verdadero rastro principal se deja en manos del volcado de WER y del registro habitual hasta justo antes del fallo. Esta es, en la práctica con aplicaciones de Windows, una forma bastante estable de trabajar.
Artículos relacionados
- Introducción a la recolección de volcados de memoria de Windows: WER/ProcDump/WinDbg
- Tabla de decisión: terminar o continuar ante una excepción inesperada
Referencias
- Microsoft Learn: Collecting User-Mode Dumps https://learn.microsoft.com/en-us/windows/win32/wer/collecting-user-mode-dumps
- Microsoft Learn: Using WER https://learn.microsoft.com/en-us/windows/win32/wer/using-wer
- Microsoft Learn: MiniDumpWriteDump function https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/nf-minidumpapiset-minidumpwritedump
- Microsoft Learn: SetUnhandledExceptionFilter function https://learn.microsoft.com/en-us/windows/win32/api/errhandlingapi/nf-errhandlingapi-setunhandledexceptionfilter
- Microsoft Learn: System.AppDomain.UnhandledException event https://learn.microsoft.com/en-us/dotnet/fundamentals/runtime-libraries/system-appdomain-unhandledexception
- Microsoft Learn: Application.ThreadException Event https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.application.threadexception
- Microsoft Learn: Application.DispatcherUnhandledException Event https://learn.microsoft.com/en-us/dotnet/api/system.windows.application.dispatcherunhandledexception
- Microsoft Learn: TaskScheduler.UnobservedTaskException Event https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskscheduler.unobservedtaskexception
- Microsoft Learn: Environment.FailFast https://learn.microsoft.com/en-us/dotnet/api/system.environment.failfast
- Microsoft Learn: Registering for Application Recovery https://learn.microsoft.com/en-us/windows/win32/recovery/registering-for-application-recovery
- Microsoft Learn: RegisterApplicationRecoveryCallback https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-registerapplicationrecoverycallback
- Microsoft Learn: WerRegisterFile https://learn.microsoft.com/en-us/windows/win32/api/werapi/nf-werapi-werregisterfile
- Microsoft Learn: _set_invalid_parameter_handler https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/set-invalid-parameter-handler-set-thread-local-invalid-parameter-handler
- Microsoft Learn: _set_purecall_handler https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/get-purecall-handler-set-purecall-handler
- Microsoft Learn: set_terminate https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/set-terminate-crt
- Microsoft Learn: __fastfail https://learn.microsoft.com/en-us/cpp/intrinsics/fastfail
- Microsoft Learn: FileStream.Flush(Boolean) https://learn.microsoft.com/en-us/dotnet/api/system.io.filestream.flush
- Microsoft Learn: !analyze (WinDbg) https://learn.microsoft.com/en-us/windows-hardware/drivers/debuggercmds/-analyze
- Microsoft Learn: .ecxr (Display Exception Context Record) https://learn.microsoft.com/en-us/windows-hardware/drivers/debuggercmds/-ecxr–display-exception-context-record-
- Microsoft Learn: Symbol path for Windows debuggers https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/symbol-path
Artículos relacionados
Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.
Introducción a la recolección de volcados de memoria de fallos en Windows - WER/ProcDump/WinDbg
Para depurar fallos difíciles de reproducir en apps Windows, se explica cuándo usar WER LocalDumps, ProcDump, MiniDumpWriteDump y WinDbg,...
Cómo interpretar los códigos de error de Windows — la estructura de tres capas de Win32, HRESULT y NTSTATUS
Antes de buscar 0x80004005, descompóngalo. Explicamos la estructura de tres capas Win32/HRESULT/NTSTATUS, el patrón 0x8007xxxx y cómo inv...
¿Qué representa realmente el «uso de memoria» de Windows? — Cómo interpretar correctamente Working Set, Private Bytes, Commit y el archivo de paginación
La memoria de Task Manager, Working Set, Private Bytes y Commit no son el mismo valor. Explica la memoria virtual y física de Windows y q...
Sincronización horaria de Windows (w32time) y los sistemas de negocio — resolver desde la raíz por qué «las marcas de tiempo de los registros no coinciden»
Explica por qué se desincronizan los relojes de un dispositivo y un PC según el funcionamiento de w32time: jerarquía de dominio, diagnóst...
El manejo de incidentes no termina con la recuperación ── el modelo de postmortem (prevención de recurrencia) para equipos de desarrollo pequeños
Si un incidente se cierra con «arreglarlo y disculparse», se repite. Adaptamos el postmortem blameless a equipos pequeños: plantilla de u...
Temas relacionados
Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.
Temas técnicos de Windows
Portal sobre desarrollo de Windows, investigación de fallos y aprovechamiento de activos existentes.
Investigación de fallos y problemas prolongados
Fallos intermitentes, diagnóstico de comunicaciones, bloqueos prolongados y pruebas de rutas de error.
Servicios relacionados con este tema
El artículo está directamente relacionado con los siguientes servicios.
Investigación de fallos y causas
Los bloqueos que solo aparecen en el entorno del cliente, las terminaciones anómalas con baja tasa de reproducción y el análisis de causas cruzando volcados de memoria con registros encajan bien con la investigación y el análisis de causas de fallos.
Desarrollo de aplicaciones para Windows
Diseñar el registro habitual, WER y el watchdog en aplicaciones WPF, WinForms, residentes y servicios de Windows está directamente ligado al propio desarrollo de aplicaciones de Windows.
Preguntas frecuentes
Preguntas habituales en las consultas sobre el tema del artículo.
- ¿Se puede garantizar que la propia aplicación que falla deje un registro?
- No se puede. Si se incluyen la corrupción de la pila, la corrupción de memoria, el fast fail, la terminación forzada e incluso el corte de energía, el último registro in-process es, por naturaleza, un best effort. En la práctica conviene separar tres capas: el registro habitual en el tiempo, el marcador final de fallo en el instante en que el proceso muere, y el rastro de fallo que deja el sistema operativo o un proceso separado. Así se evita depender solo de lo que ocurre dentro del proceso que se está cayendo. La combinación más segura es registro habitual + marcador final de fallo + WER LocalDumps.
- ¿Qué no se debe hacer dentro del manejador de fallos?
- Cualquier procesamiento pesado en general. Resolver un logger desde un contenedor de DI, usar async/await, esperar un bloqueo, generar JSON complejo, manipular objetos COM, mostrar cuadros de diálogo de la interfaz, comprimir datos o enviar por HTTP/SMTP/Slack son, con alta probabilidad, minas. Lo que sí hay que hacer se reduce a cuatro cosas: evitar la reentrada, escribir una sola línea, hacer flush y terminar. El posprocesamiento pesado, como comprimir, subir o notificar, se deja para el siguiente arranque o para otro proceso.
- ¿Cómo se debe configurar WER LocalDumps?
- En el registro, bajo HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\nombre-de-la-app.exe, se configuran DumpFolder (una carpeta dedicada), DumpCount (entre 5 y 10 aproximadamente) y DumpType (2 en máquinas de desarrollo; 1 o 2 en campo, según el espacio disponible y los requisitos de confidencialidad). Lo importante es verificar la ACL de la carpeta de destino: un error típico es apuntar a una carpeta en la que un servicio de Windows o una cuenta restringida no pueden escribir, con lo que la captura queda en blanco. Además, la recolección de volcados y la conservación de los PDB y de los binarios distribuidos van siempre juntas: si falta cualquiera de las dos, luego no se puede interpretar el volcado.
- ¿Es correcto atrapar la excepción en DispatcherUnhandledException de WPF y continuar?
- Es peligroso frente a excepciones inesperadas originadas por un error de programación. Con Handled = true se puede continuar en apariencia, pero es fácil terminar en un estado zombi: la pantalla sigue en pie mientras el worker ya murió, o los botones siguen activos sin saber si el guardado se completó. Conviene usar el evento de excepción no controlada como entrada de registro, no como mecanismo de recuperación: una vez escrito el marcador final de fallo, es más seguro terminar el proceso en lugar de continuar, y dejar que el rastro principal quede en el volcado de WER y en el registro habitual hasta ese momento. Para terminar, conviene evaluar una API de terminación inmediata como Environment.FailFast.
Perfil del autor
Página de presentación del autor del artículo.
Go Komura
Representante de KomuraSoft LLC
Especializado en desarrollo de software para Windows, consultoría técnica e investigación de fallos, sobre todo en proyectos con sistemas existentes y errores difíciles de reproducir.