TheAlgorithms/Java · error · IllegalArgumentException

TTL must be >= 0

Error message

TTL must be >= 0

What it means

Thrown by LIFOCache.put(K, V, long ttlMillis) when ttlMillis is negative. Expiry is computed as now + ttlMillis, so a negative TTL sets expiry in the past and makes the entry instantly invalid. The check runs after the null guard, before lock acquisition.

Source

Thrown at src/main/java/com/thealgorithms/datastructures/caches/LIFOCache.java:175

    /**
     * Adds a key-value pair to the cache with a specified time-to-live (TTL).
     *
     * <p>If the key already exists, its value is removed, re-inserted at tail and its TTL is reset.
     * If the key does not exist and the cache is full, the youngest entry is evicted to make space.
     * Expired entries are also cleaned up prior to any eviction. The eviction listener
     * is notified when an entry gets evicted.
     *
     * @param key        the key to associate with the cached value; must not be {@code null}
     * @param value      the value to be cached; must not be {@code null}
     * @param ttlMillis  the time-to-live for this entry in milliseconds; must be >= 0
     * @throws IllegalArgumentException if {@code key} or {@code value} is {@code null}, or if {@code ttlMillis} is negative
     */
    public void put(K key, V value, long ttlMillis) {
        if (key == null || value == null) {
            throw new IllegalArgumentException("Key and value must not be null");
        }
        if (ttlMillis < 0) {
            throw new IllegalArgumentException("TTL must be >= 0");
        }

        lock.lock();
        try {
            // If key already exists, remove it. It will later be re-inserted at top of stack
            keys.remove(key);
            final CacheEntry<V> oldEntry = cache.remove(key);
            if (oldEntry != null && !oldEntry.isExpired()) {
                notifyEviction(key, oldEntry.value);
            }

            // Evict expired entries to make space for new entry
            evictExpired();

            // If no expired entry was removed, remove the youngest
            if (cache.size() >= capacity) {
                final K youngestKey = keys.pollLast();
                final CacheEntry<V> youngestEntry = cache.remove(youngestKey);

View on GitHub (pinned to fdfb9a395b)

Solutions

  1. Clamp the TTL: long ttl = Math.max(0, ttlMillis);
  2. Treat negative TTL as 'no expiry' or 'skip caching' per your domain, explicitly.
  3. Validate all TTL config keys at startup.

Example fix

// before
cache.put(k, v, duration.toMillis());
// after
long ttl = duration.toMillis();
if (ttl < 0) throw new IllegalStateException("negative ttl: " + duration);
cache.put(k, v, ttl);
Defensive patterns

Strategy: validation

Validate before calling

long ttl = Math.max(0, ttlMillis);
cache.put(key, value, ttl);

Type guard

static boolean isValidTtl(long ttlMillis) {
    return ttlMillis >= 0;
}

Prevention

When it happens

Trigger: cache.put(k, v, -1); ttlMillis from a negative Duration.toMillis(); clock-skew arithmetic yielding a negative delta; config typo with a leading minus.

Common situations: TTL from request headers with bad client clocks; env config (cache.ttl=-5000) typoed; reused duration variable inverted elsewhere.

Related errors


AI-assisted analysis of TheAlgorithms/Java@fdfb9a395b (2026-08-13). Data as JSON: /api/errors/33a993957f4fff25. Report an issue: GitHub.