En resumen: PHP comunica los problemas por dos vías: como errores clásicos, como advertencias y avisos, que se controlan con error_reporting, y como excepciones, que se capturan con try y catch. Desde PHP 7, muchos errores internos también lanzan un objeto de la clase Error; junto con Exception, implementa la interfaz Throwable. En producción rige lo siguiente: registrar los errores en el log y no mostrarlos nunca en el navegador. En PHP 8.5, a causa de PHP2Shell, fatal_error_backtraces=0 forma además parte de las medidas provisionales.
Dos mundos: errores clásicos y excepciones
PHP viene de una época en la que los errores se emitían sobre todo como mensajes: avisos (E_NOTICE), advertencias (E_WARNING), funciones obsoletas (E_DEPRECATED) y errores fatales (E_ERROR). Muchos de estos mensajes dejan que el script siga ejecutándose, a menudo con datos incorrectos o vacíos.
Las excepciones funcionan de otro modo. Interrumpen el flujo normal y saltan al siguiente bloque catch que encaje. Si no se encuentra ninguno, el script termina. Para quienes toman decisiones, la diferencia es importante: una advertencia silenciosa puede hacer que un pedido se guarde de forma incompleta sin que nadie lo note. Una excepción hace visible el problema y permite tratarlo.
Una utilidad infravalorada de los mensajes clásicos son los avisos de tipo E_DEPRECATED. Anuncian que un recurso del lenguaje desaparecerá en una versión futura de PHP. Quien los recoge en el log y los evalúa con regularidad sabe, antes de una actualización, qué puntos hay que adaptar, y no tiene que probar durante mucho tiempo cuando surge una vulnerabilidad urgente.
La jerarquía Throwable
Todo lo que se puede lanzar con throw y capturar con catch implementa Throwable. De ahí cuelgan dos ramas:
| Clase | Finalidad | Ejemplos de subclases |
|---|---|---|
Exception |
errores previsibles de la aplicación y de las extensiones | ErrorException, PDOException, clases propias |
Error |
errores internos de PHP | TypeError, ValueError, ArgumentCountError, ArithmeticError, DivisionByZeroError, UnhandledMatchError |
Las clases de error propias derivan de Exception, no de Error. Desde PHP 8.0, la mayoría de las funciones internas lanzan un TypeError o un ValueError ante parámetros no válidos, en lugar de emitir solo una advertencia. strlen([]) es uno de esos casos.
try, catch y finally en la práctica
<?php
declare(strict_types=1);
final class ZahlungFehlgeschlagen extends RuntimeException {}
function bezahlen(Bestellung $b, Zahlungsdienst $dienst, LoggerInterface $log): bool
{
try {
$dienst->belasten($b->betrag(), $b->referenz());
return true;
} catch (ZahlungFehlgeschlagen $e) {
$log->warning('Zahlung abgelehnt', ['bestellung' => $b->id(), 'exception' => $e]);
return false;
} catch (TimeoutException | VerbindungsFehler) {
// desde PHP 8.0 la variable en el catch es opcional
$log->error('Zahlungsdienst nicht erreichbar', ['bestellung' => $b->id()]);
return false;
} finally {
$dienst->verbindungSchliessen();
}
}
Varias clases se pueden agrupar en un catch con |. El bloque finally se ejecuta siempre, haya ocurrido un error o no. Desde PHP 8.0, además, throw es una expresión y puede aparecer, por ejemplo, tras ??: $id = $_GET['id'] ?? throw new InvalidArgumentException('id fehlt');.
Handlers globales: set_error_handler y set_exception_handler
Dos funciones interceptan lo que de otro modo se escaparía. set_exception_handler() registra una función para los Throwable no capturados. Según el manual, el script termina después de llamarla; sirve, por tanto, para registrar en el log y mostrar una página de error neutra, no para seguir trabajando.
Ha dado buen resultado un handler que escribe en el log el error completo con la hora y un identificador aleatorio, y que al visitante solo le muestra una página neutra con ese identificador. El soporte puede entonces localizar el caso en el log sin que salgan al exterior detalles internos.
set_error_handler() captura los errores clásicos. El manual muestra cómo convertirlos en una ErrorException, de modo que las advertencias se traten como excepciones:
<?php
set_error_handler(function (int $nr, string $text, string $datei, int $zeile): bool {
if (!(error_reporting() & $nr)) {
return false; // excluido por error_reporting
}
if ($nr === E_DEPRECATED || $nr === E_USER_DEPRECATED) {
return false; // no escalar los avisos de funciones obsoletas
}
throw new ErrorException($text, 0, $nr, $datei, $zeile);
});
Los errores fatales, como un desbordamiento de memoria, no se pueden capturar por esta vía. Qué ocurre en el interior durante un desbordamiento lo explica la página memory_limit y Out-of-Memory.
Configurar la salida de errores y el registro
Los ajustes más importantes con sus valores por defecto según el manual:
| Ajuste | Valor por defecto | Recomendación en producción |
|---|---|---|
error_reporting |
E_ALL desde PHP 8.0.0 |
E_ALL |
display_errors |
1 |
Off |
display_startup_errors |
1 desde PHP 8.0.0 |
Off |
log_errors |
0 |
On |
error_log |
vacío (log del servidor web o de la SAPI) | archivo propio fuera del webroot |
fatal_error_backtraces |
1, nuevo en PHP 8.5.0 |
provisionalmente 0 (PHP2Shell) |
Desde PHP 8.2.0, error_log_mode fija los permisos del archivo de log, por defecto 0644. Si los logs contienen datos personales o rutas, conviene un valor más estricto para que otros usuarios del servidor no puedan leerlos. Con ignore_repeated_errors se pueden agrupar mensajes idénticos del mismo archivo y línea, para que un bucle de errores no inunde el log.
El php.ini-production incluido ya pone display_errors en Off. Según el manual, ini_set('display_errors', ...) no actúa en tiempo de ejecución sobre los errores fatales, y los errores de sintaxis ocurren antes de que se ejecute cualquier ini_set(). Por eso estos valores pertenecen a la configuración del servidor. Los antecedentes figuran en el artículo Desactivar la salida de errores en producción; cómo llevar los logs sin filtrar datos, en el artículo Registrar logs con seguridad.
Tratamiento de errores y PHP2Shell
Para PHP 8.5, php.net indica como una de las medidas provisionales frente a PHP2Shell poner fatal_error_backtraces=0 y reiniciar PHP después. Las demás medidas afectan a memory_limit, post_max_size y session.upload_progress.enabled. Ninguna de ellas sustituye a la actualización a PHP 8.5.12, 8.4.27, 8.3.36 o 8.2.35.
; solo PHP 8.5, en php.ini o en el pool de FPM; después, reiniciar todos los workers
fatal_error_backtraces = 0
Además, tras PHP2Shell los logs de errores son una prueba importante. Si a partir del 20/10/2026 se acumulan errores de memoria o workers interrumpidos, puede indicar intentos de ataque, aunque también puede tener causas inofensivas. Conserve los logs y evalúelos junto con los logs de acceso, como se describe en la guía Analizar los logs del servidor web tras un ataque.
¿Cómo ayuda PHP SOS en concreto?
- Revisamos los ajustes efectivos de salida de errores y de registro de cada sitio web y los dejamos aptos para producción.
- Aplicamos las medidas provisionales de PHP2Shell adecuadas a la versión de PHP y reiniciamos todos los workers.
- Aseguramos los logs de errores y de acceso desde el 20/10/2026 y los analizamos en busca de anomalías.
- Como parte de la monitorización de seguridad avisamos de acumulaciones inusuales de errores, en lugar de dejar que se pierdan en el log.
Preguntas frecuentes
¿Qué diferencia hay entre Error y Exception?
Ambas implementan la interfaz Throwable. Exception está pensada para errores que una aplicación espera y gestiona, como una conexión fallida con la base de datos. Error representa los errores internos de PHP, como un TypeError o una división por cero.
¿Debe estar activado display_errors en producción?
No. Los mensajes de error en el navegador revelan rutas, consultas y, a veces, credenciales. En producción, display_errors debe estar en Off y log_errors en On, para que los errores acaben en el log y no ante el visitante.
¿Se puede capturar un desbordamiento de memoria con try/catch?
No. Si un script supera memory_limit, PHP lo termina con un error fatal que no se puede capturar como excepción. Solo aparece en el log de errores, siempre que log_errors esté activo.
¿Qué es fatal_error_backtraces?
Un ajuste que existe desde PHP 8.5.0 y que añade un backtrace a los errores fatales. Está activo por defecto. Como medida provisional frente a PHP2Shell, php.net indica para PHP 8.5 ponerlo a 0 y reiniciar PHP después.