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

  1. 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).
  2. 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.
  3. 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.
  4. 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.
  5. 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

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.