php-fig/log · error · Psr\Log\InvalidArgumentException
Psr\Log\InvalidArgumentException
Error message
Psr\Log\InvalidArgumentException
What it means
Psr\Log\InvalidArgumentException is the PSR-3 logger-interface standard exception for invalid arguments passed to a PSR-3 logger. The class itself (src/InvalidArgumentException.php:5) is just an empty marker extending PHP's SPL \InvalidArgumentException; psr/log never throws it directly. Instead, LoggerInterface::log() declares '@throws \Psr\Log\InvalidArgumentException' (src/LoggerInterface.php:95), and concrete implementations (Monolog being the most common) throw it when the $level argument is not a valid PSR-3 / RFC 5424 level name: 'emergency', 'alert', 'critical', 'error', 'warning', 'notice', 'info', 'debug'.
Source
Thrown at src/InvalidArgumentException.php:5
<?php
namespace Psr\Log;
class InvalidArgumentException extends \InvalidArgumentException
{
}
View on GitHub (pinned to f16e1d5863)
Solutions
- Switch to the dedicated shorthand methods or the Psr\Log\LogLevel constants so the level is always a known-valid literal: $logger->error($msg) or $logger->log(LogLevel::ERROR, $msg).
- If the level comes from config/env, normalize and whitelist it before logging: lowercase it and check it against the eight LogLevel constants, substituting a safe default for unknown values.
- If your app has its own severity vocabulary, map each internal level explicitly to a PSR-3 level with a lookup table instead of passing it through raw.
- After a Monolog major-version upgrade (2 -> 3), audit every log()/addRecord() call site: numeric levels became Monolog\Level\* enum cases and unrecognized names now throw this exception.
- Catch Psr\Log\InvalidArgumentException around log() only as a last-resort safety net so one bad level string cannot take down the request.
Example fix
// before
$level = getenv('LOG_LEVEL'); // e.g. 'ERROR' or 'verbose' -> throws
$logger->log($level, 'Payment failed', ['id' => $paymentId]);
// after
use Psr\Log\LogLevel;
$validLevels = [
LogLevel::EMERGENCY, LogLevel::ALERT, LogLevel::CRITICAL,
LogLevel::ERROR, LogLevel::WARNING, LogLevel::NOTICE,
LogLevel::INFO, LogLevel::DEBUG,
];
$level = strtolower((string) getenv('LOG_LEVEL'));
if (!in_array($level, $validLevels, true)) {
$level = LogLevel::INFO; // safe default for bad config
}
$logger->log($level, 'Payment failed', ['id' => $paymentId]); Defensive patterns
Strategy: validation
Validate before calling
use Psr\Log\LogLevel;
const PSR3_LEVELS = [
LogLevel::EMERGENCY, LogLevel::ALERT, LogLevel::CRITICAL,
LogLevel::ERROR, LogLevel::WARNING, LogLevel::NOTICE,
LogLevel::INFO, LogLevel::DEBUG,
];
function normalizePsr3Level(mixed $level): string
{
if (is_string($level)) {
$level = strtolower($level);
if (in_array($level, PSR3_LEVELS, true)) {
return $level;
}
}
return LogLevel::INFO; // safe default, never throws
}
// before calling log():
$logger->log(normalizePsr3Level($configLevel), $message, $context); Type guard
use Psr\Log\LogLevel;
function isPsr3Level(mixed $level): bool
{
return is_string($level)
&& in_array($level, [
LogLevel::EMERGENCY, LogLevel::ALERT, LogLevel::CRITICAL,
LogLevel::ERROR, LogLevel::WARNING, LogLevel::NOTICE,
LogLevel::INFO, LogLevel::DEBUG,
], true);
}
// usage:
if (isPsr3Level($level)) {
$logger->log($level, $message, $context);
} else {
$logger->info($message, $context + ['invalid_level' => $level]);
} Try / catch
use Psr\Log\InvalidArgumentException as LogInvalidArgumentException;
try {
$logger->log($level, $message, $context);
} catch (LogInvalidArgumentException $e) {
// degrade gracefully: re-log at a guaranteed-valid level, keep the bad value in context
$logger->warning('Dropped log with invalid level {level}: {reason}', [
'level' => is_scalar($level) ? (string) $level : get_debug_type($level),
'reason' => $e->getMessage(),
'exception' => $e,
]);
} Prevention
- Prefer the shorthand methods ($logger->error(), $logger->warning(), ...) over log(); they cannot receive an invalid level.
- Always use the Psr\Log\LogLevel constants instead of hand-typed level strings; they eliminate case and spelling mistakes.
- Never pass a raw config value, env var, DB field, or queue payload as $level without whitelist validation first.
- Remember PSR-3 levels are lowercase only: 'ERROR' and 'Error' both throw; normalize with strtolower() before checking.
- Add a startup/boot check that asserts every configured level constant is one of the eight valid names, failing fast with a clear config error.
- When upgrading Monolog across major versions (2 -> 3), grep for ->log( and numeric level constants; the level representation changed and old values now throw.
When it happens
Trigger: Calling $logger->log($level, $message) where $level is not one of the eight PSR-3 level strings: an unknown name like $logger->log('verbose', 'msg'), a wrong-case name like $logger->log('ERROR', 'msg') (levels are lowercase only), an integer severity (e.g. a syslog priority or old Monolog int constant), null, or an empty string. The level most often arrives dynamically from a config file, environment variable, database column, or message queue payload, so the invalid value only surfaces at runtime. The eight shorthand methods ($logger->error(), $logger->info(), ...) never hit this path because they hard-code a valid level.
Common situations: Config-driven log levels: a .env or YAML value like LOG_LEVEL=ERROR (uppercase) fed straight into log(). Monolog upgrades: Monolog 2 used integer level constants while Monolog 3 moved to a Level enum, so code passing numeric levels or calling Logger::addRecord with stale values breaks across major versions. Passing a custom/application-specific severity vocabulary ('notice', 'trace', 'fatal', 'verbose') that Monolog does not register. Case mismatch between LogLevel::ERROR ('error') and human-readable 'Error'/'ERROR' strings coming from logs APIs or user input.
AI-assisted analysis of php-fig/log@f16e1d5863 (2026-08-21).
Data as JSON: /api/errors/ecd68f67867c6ee3.
Report an issue: GitHub.