Logging Errors
Displaying an error is only useful for the person staring at the screen at the exact moment it happens — which, for a production website, is almost never the developer. Logging is what turns a transient failure into a record someone can actually investigate later: what happened, when, on which request, with what data. PHP's built-in logging is a reasonable starting point, but real applications quickly outgrow it, and understanding why is as important as knowing the function signatures.
error_log(): the basic building block
error_log() writes a single message to wherever PHP is configured to send errors — a file, the system log, or (rarely, and not recommended for production) email. Its destination is controlled by the error_log ini setting and the function's own optional arguments.
Logging a caught exception
<?php
try {
processPayment(150.0);
} catch (\RuntimeException $e) {
// Goes to whatever destination php.ini's error_log points at.
error_log('Payment processing failed: ' . $e->getMessage());
}Overriding the destination for one call
<?php
// message_type 3 = append to the given file, ignoring the ini default.
error_log(
'Payment processing failed: insufficient funds',
3,
'/var/log/php/payments.log',
);File logging vs syslog
Writing to a plain file is the simplest option and works well for a single server: tail -f the file, grep through it, rotate it with logrotate. It stops working well the moment there's more than one server, though — logs get scattered across machines with no unified place to search them. Syslog (via error_log()'s message_type = 0, which routes through the operating system's own logging facility, or PHP's syslog() function directly) solves that by handing log entries to a system service that can forward them to a central collector.
Sending a message through syslog instead of a file
<?php
openlog('my-app', LOG_PID, LOG_USER);
syslog(LOG_ERR, 'Payment processing failed: insufficient funds');
closelog();The trade-off is operational, not code complexity: syslog requires a properly configured syslog daemon (or a forwarder shipping to something like an ELK stack or a hosted log service) to be worth the extra setup. For a single small application, a rotated file is often genuinely simpler and sufficient.
Why plain error_log() strings don't scale
A single error_log('Payment processing failed: insufficient funds') call is fine in isolation. The problem shows up once an application has hundreds of call sites like it: every message is a free-form string, so searching, filtering, and alerting all depend on grepping text and hoping the wording stayed consistent. There's no reliable way to ask "show me every error at severity ERROR from the payments module in the last hour" when severity and module are just substrings buried inside prose, formatted slightly differently by whoever wrote each call site.
Structured logging: severity, message, and context as separate fields
<?php
function logError(string $message, array $context = []): void
{
$entry = [
'level' => 'error',
'message' => $message,
'context' => $context,
'timestamp' => date(DATE_ATOM),
];
error_log(json_encode($entry));
}
logError('Payment processing failed', [
'orderId' => 42,
'amount' => 150.0,
'reason' => 'insufficient_funds',
]);{"level":"error","message":"Payment processing failed","context":{"orderId":42,"amount":150,"reason":"insufficient_funds"},"timestamp":"2026-07-02T10:15:00+00:00"}Once every log line is structured JSON like this, a log aggregation tool can index level, context.orderId, and context.reason as real, queryable fields instead of text to search. That's the whole idea behind "structured logging" — the format is machine-parseable first, human-readable second.
PSR-3 and Monolog
Rather than every project inventing its own logError() helper (like the one above), the PHP ecosystem standardized on PSR-3, an interface specification (Psr\\Log\\LoggerInterface) defining methods like emergency(), error(), warning(), and info() — one per RFC 5424 severity level — each accepting a message and a context array. Any library or framework that accepts "a PSR-3 logger" can be handed any object implementing that interface, without caring what actually writes the log entries underneath.
Code written against the PSR-3 interface, not a concrete logger
<?php
use Psr\Log\LoggerInterface;
class PaymentProcessor
{
public function __construct(private readonly LoggerInterface $logger)
{
}
public function process(float $amount): void
{
try {
// ... charge logic ...
} catch (\RuntimeException $e) {
$this->logger->error('Payment processing failed', [
'amount' => $amount,
'exception' => $e,
]);
}
}
}Monolog is the ecosystem-standard implementation of LoggerInterface — it's what actually receives those error() calls and routes them to one or more "handlers": a rotating file, syslog, a Slack webhook for critical failures, an external service like Sentry, or several of these simultaneously. Because PaymentProcessor above only depends on the interface, swapping Monolog's configuration (or swapping Monolog for a different PSR-3 implementation entirely) never requires touching the classes that do the logging.
error_log()is PHP's built-in primitive for writing a single message to a file, syslog, or another sink.File logs are simplest for one server; syslog (or a log aggregator) becomes worthwhile once logs span multiple machines.
Free-form message strings don't scale -- structured logging (level, message, and a context array/JSON) makes logs queryable.
PSR-3's
LoggerInterfacestandardizes the logging API so application code never depends on a specific logging library.Monolog is the ecosystem-standard PSR-3 implementation, routing log calls to files, syslog, Slack, or external services via handlers.