quarkusio/quarkus · error · IllegalArgumentException

Cannot cache `null` value

Error message

Cannot cache `null` value

What it means

RedisCacheImpl does not allow caching null values: when a value loader completes with null, the cache throws IllegalArgumentException instead of storing a null (unlike the local Caffeine cache which permits nulls). The check happens right before marshalling, since null cannot be encoded for Redis storage.

Source

Thrown at extensions/redis-cache/runtime/src/main/java/io/quarkus/cache/redis/runtime/RedisCacheImpl.java:193

                return startingPoint
                        .chain(Unchecked.function(new UncheckedFunction<>() {
                            @Override
                            public Uni<V> apply(V cached) throws Exception {
                                if (cached != null) {
                                    // Unwatch if optimistic locking
                                    if (cacheInfo.useOptimisticLocking) {
                                        return connection.send(Request.cmd(Command.UNWATCH))
                                                .replaceWith(cached);
                                    }
                                    return Uni.createFrom().item(new StaticSupplier<>(cached));
                                } else {
                                    Uni<V> uni = computeValue(key, valueLoader, isWorkerThread);

                                    return uni.onItem().call(new Function<V, Uni<?>>() {
                                        @Override
                                        public Uni<?> apply(V value) {
                                            if (value == null) {
                                                throw new IllegalArgumentException("Cannot cache `null` value");
                                            }
                                            byte[] encodedValue = marshaller.encode(value);
                                            Uni<V> result;
                                            if (cacheInfo.useOptimisticLocking) {
                                                result = multi(connection, set(connection, encodedKey, encodedValue))
                                                        .replaceWith(value);
                                            } else {
                                                result = set(connection, encodedKey, encodedValue).replaceWith(value);
                                            }
                                            if (isWorkerThread) {
                                                return result.runSubscriptionOn(
                                                        MutinyHelper.blockingExecutor(vertx.getDelegate(), false));
                                            }
                                            return result;
                                        }
                                    });
                                }
                            }

View on GitHub (pinned to e1c734241f)

Solutions

  1. Make the loader return a non-null sentinel or Optional and unwrap after retrieval
  2. Use getOrNull(...) semantics: store a marker EMPTY value and map back to null on read
  3. Change the method/logic to not return null (throw a domain-specific 'not found' or return a default)
  4. Use the local (Caffeine) cache type instead of redis for caches that must allow null values

Example fix

// before
V v = cache.get(key, k -> maybeNullLoader(k));
// after
Optional<V> opt = cache.get(key, k -> Optional.ofNullable(maybeNullLoader(k)));
V v = opt.orElse(null);
Defensive patterns

Strategy: try-catch

Validate before calling

V value = loader.apply(key);
if (value == null) throw new IllegalStateException("Loader must not return null for redis cache");

Try / catch

try {
    V v = cache.get(key, loader);
} catch (IllegalArgumentException e) {
    if (e.getMessage().contains("Cannot cache `null` value")) {
        // treat as cache miss / not-found and fall back
    } else throw e;
}

Prevention

When it happens

Trigger: Calling RedisCache.get/getAsync (non-getOrNull variants) with a compute function or value loader that returns null; @CacheResult on a method that returns null; a Uni chain resolving to null item.

Common situations: Database lookups that legitimately return no row; optional data fetched by key; migrating from Caffeine/local cache to Redis cache where null-tolerant behavior previously worked.

Related errors


AI-assisted analysis of quarkusio/quarkus@e1c734241f (2026-09-05). Data as JSON: /api/errors/d5f707914d12e8d2. Report an issue: GitHub.