Si eres de los que hace "log" de los errores que se producen en tu aplicación para enterarte de cuando pasa algo (y deberías) es posible que, en un momento determinado, empieces a registrar errores como el del título de este post, con mensajes estilo este:

System.Threading.ThreadAbortException: Thread was being aborted.
at System.Threading.Thread.AbortInternal()
at System.Threading.Thread.Abort(Object stateInfo)
at System.Web.HttpResponse.End()
at TuAplicacion.Page_Load(Object sender, EventArgs e) in C:\AppsWeb\TuApp\pagina.aspx.cs:line 19

Sin embargo, en la interfaz de usuario jamás llegas a ver ningún error, ni tus usuarios se han quejado nunca de que lo haya. Pero en los logs sigue apareciendo una y otra vez...

¿A qué es debido?

Bueno, el mensaje no es muy detallado que digamos, pero si le sigues la pista al código seguro que te vas a encontrar en el archivo y línea indicados, con algo como esto:

Response.Redirect("~/otra-pagina.aspx");

Una inofensiva y súper-común redirección de respuesta a otra página. Entonces, ¿por qué se produce una excepción?

El motivo es que el método Redirect() tiene dos sobrecargas. Ambas toman como primer argumento a dónde se debe redirigir la petición (con un estado HTTP 302, de redirección temporal). La otra sobrecarga toma como segundo parámetro un booleano para decidir si además se debe abortar o no el proceso de la página actual. Pero la sobrecarga sencilla anterior (solo el destino), que es la que usa casi todo el mundo, es equivalente a pasar un true en ese segundo parámetro. Es decir, lo anterior es equivalente a esto:

Response.Redirect("~/otra-pagina.aspx", true);

Lo que esto significa es que esa linea no solo envía una cabecera de redirección al cliente, (hasta que este la recibe no se produce la redirección), sino que además, para que llegue cuanto antes, finaliza la ejecución de la página llamando a Reponse.End(). Es decir, en realidad esa inofensiva línea es equivalente a esto:

Response.Redirect("~/otra-pagina.aspx", false);
Response.End();

Si examinamos con un decompilador qué hace exactamente Response.End() veremos el siguiente código:

El código de Response.End

Como podemos observar lo que ocurre es que, directamente llama a AbortCurrentThread(). O sea, se carga el hilo de ejecución actual, impidiendo que la página pase por todas las fases restantes del pipeline de la misma e impidiendo, por ejemplo, que pueda liberar ciertos recursos, etc.. Además, al hacerlo se genera un error de tipo System.Threading.ThreadAbortException que estamos viendo.

Lo peor es que este tipo de error, por definición, no podemos catpurarlo ya que aunque rodeemos el código con un try-catch, al "matarse" el hilo de ejecución ya no podrá ejecutarse nunca el catch. Como la página se redirige a otro lado el usuario nunca llega a ver el error, pero está ahí. Además, esto incide directamente en el rendimiento de la aplicación y viola el conocido como Principio de la mínima sorpresa o Principle of least astonishment (POLA) que, aunque se suele aplicar al diseño de interfaces de usuario, en este caso siendo algo de programación, es aplicable puesto que el código no se está comportando como cabría esperar (ese error no te lo esperas y ni siquiera aparece).

Solución

Ya está bien de explicaciones "sesudas". Vamos a solucionarlo.

La solución pasa hacer esto:

Response.Redirect("~/otra-pagina.aspx", false);
Context.ApplicationInstance.CompleteRequest();

Es decir, llamar a Redirect() para que coloque en la respuesta las cabeceras HTTP adecuadas para la redirección, y luego terminar la ejecución de la página adecuadamente, llamando al método CompleteRequest() de la instancia actual de la aplicación, que lo que hace es terminar adecuadamente la ejecución de la página saltando directamente al evento EndRequest del pipeline de la página.

Por supuesto, si tenemos el código estructurado de manera que tras el Redirect() ya no hay código que ejecutar, entonces la la página terminará normalmente y no se producirá error, por lo que no necesitamos esa instrucción adicional.

En mi opinión deberían haber puesto false como valor por defecto para la sobrecarga sencilla de Redirect(). Lo malo es que supongo que esto habría provocado muchos errores en código directamente heredado de ASP Clásico, que era una cosa muy importante cuando salió .NET hace casi 20 años. Así que imagino que lo hicieron así para evitar problemas en las migraciones y facilitar la adopción de la nueva plataforma. Pero como esto es algo que no mucha gente conoce, sigue dando problemas dos décadas después 😣 De todos modos, supongo que podrían haber añadido una llamada a CompleteRequest sin mucho problema, pero si no lo hicieron seguro que hay algo que se me escapa.

En resumen

En general no deberíamos utilizar nunca (o casi nunca) Response.End() o métodos que indirectamente llamen a este método, porque ocurre lo que vimos al principio: un error de hilo abortado. Este método se metió en su día por compatibilidad con ASP 3.0 "clásico", donde sí tenía más sentido dadas sus limitaciones, pero que hoy en día en realidad no se me ocurren muchos casos en donde pueda ser necesario de verdad. Así que ya sabes: evítalo y todos los que lo llamen, como Response.Redirect(). Evitarás producir errores (y loguearlos), mejoraarás el rendimiento de la aplicación (ligeramente) y evitarás "sorpresas".

¡Espero que te resulte útil!

Escrito por un humano, no por una IA