ben-manes/caffeine · error · NullPointerException

null map

Error message

null map

What it means

LocalAsyncCache.resolve joins the future returned by an async bulk mapping function; if it completed with a null map (signaled by the internal NullMapCompletionException), resolve converts it into NullPointerException("null map"). Bulk loads must return a map (an empty one if nothing was loaded) — null is a contract violation, matching the Map/ConcurrentMap rule that null collections are forbidden.

Source

Thrown at caffeine/src/main/java/com/github/benmanes/caffeine/cache/LocalAsyncCache.java:693

      return resolve(asyncCache().get(key, mappingFunction));
    }

    @Override
    public Map<K, V> getAll(
        Iterable<? extends K> keys,
        Function<
            ? super Set<? extends K>,
            ? extends Map<? extends K, ? extends V>> mappingFunction) {
      return resolve(asyncCache().getAll(keys, mappingFunction));
    }

    @SuppressWarnings({"PMD.AvoidThrowingNullPointerException",
      "PMD.PreserveStackTrace", "UnusedException"})
    protected static <T> T resolve(CompletableFuture<T> future) {
      try {
        return future.join();
      } catch (NullMapCompletionException e) {
        throw new NullPointerException("null map");
      } catch (CompletionException e) {
        if (e.getCause() instanceof RuntimeException) {
          throw (RuntimeException) e.getCause();
        } else if (e.getCause() instanceof Error) {
          throw (Error) e.getCause();
        }
        throw e;
      }
    }

    @Override
    public void put(K key, V value) {
      requireNonNull(value);
      asyncCache().cache().put(key, CompletableFuture.completedFuture(value));
    }

    @Override
    public void putAll(Map<? extends K, ? extends V> map) {

View on GitHub (pinned to 9da6581ee3)

Solutions

  1. Return an empty map (or a map covering the requested keys) instead of null from loadAll/asyncLoadAll and from getAll mapping functions
  2. Wrap third-party calls: return result != null ? result : Map.of();
  3. Treat 'no data found' as empty, not null — absent keys are simply missing from the result

Example fix

// before
CompletableFuture<Map<K, V>> f = CompletableFuture.completedFuture(repo.findAll(keys)); // null when empty

cache.getAll(keys, f::join... ); // NullPointerException: null map

// after
var found = repo.findAll(keys); // may be null
return CompletableFuture.completedFuture(found == null ? Map.<K, V>of() : found);
Defensive patterns

Strategy: validation

Validate before calling

// Null-safe the mapping result before it reaches getAll:
static <K, V> CompletableFuture<Map<K, V>> nullSafe(CompletableFuture<Map<K, V>> f) {
  return f.thenApply(m -> (m == null) ? Map.<K, V>of() : m);
}
cache.getAll(keys, ks -> nullSafe(bulkLoad(ks)));

Try / catch

try {
  return cache.synchronous().getAll(keys);
} catch (NullPointerException e) {
  if ("null map".equals(e.getMessage())) {
    // loader returned null: treat as empty and continue
    return Map.of();
  }
  throw e;
}

Prevention

When it happens

Trigger: cache.getAll(keys, mappingFunction) where mappingFunction's future completes with null; a CacheLoader/AsyncCacheLoader whose loadAll/asyncLoadAll returns null (e.g. from a repository returning null on empty results).

Common situations: DAO/repository layers that signal "not found" with null instead of an empty map; Optional.map(...) chains that yield null futures; third-party bulk APIs returning null for empty input sets.

Related errors


AI-assisted analysis of ben-manes/caffeine@9da6581ee3 (2026-08-14). Data as JSON: /api/errors/4c2787b20d63c5fe. Report an issue: GitHub.