The most analyzed error in production Java
When we looked at the error distribution across anonymized analyses run through ErrorLens, one error type appeared more than any other: java.lang.NullPointerException. It appeared in 34% of all Java-related analyses, far ahead of the second-most-common error type.
This isn't surprising. NPEs are the famously vague Java error that tells you something is null but doesn't always tell you what. They're the error that has frustrated Java developers for decades. Java 14 improved this with "helpful NullPointerExceptions" that name the specific variable, but legacy codebases still run on older JVMs, and even with JDK 14+ the error doesn't tell you why the value was null.
After analyzing the patterns, we found five root causes that account for 91% of all NPEs in our dataset. Understanding these patterns doesn't just help you fix the error in front of you — it helps you write code that produces NPEs far less often in the first place.
Pattern 1: Unguarded repository or database lookups (38% of NPEs)
The most common pattern. A method retrieves a record from a database or repository and immediately calls methods on it without checking whether the record was found:
// ❌ NPE waiting to happen
User user = userRepository.findById(userId);
String email = user.getEmail(); // NPE if user is null (not found)
This appears in virtually every Java codebase. The subtle version is when the null case genuinely seems impossible in context — "we just created this user, there's no way findById returns null here" — and it turns out to be wrong under a race condition, a soft-delete edge case, or a test database state.
The fix: Return Optional<T> from repository methods and force the caller to handle the empty case explicitly:
// ✅ Explicit handling
Optional<User> userOpt = userRepository.findById(userId);
String email = userOpt
.map(User::getEmail)
.orElseThrow(() -> new UserNotFoundException(userId));
Better: annotate your repository interface with @NonNull and @Nullable (via Lombok or Jakarta Validation) at the method signature level. This lets IDEs and static analysis tools surface the risk at write time rather than runtime.
Pattern 2: Null returns from third-party APIs and HTTP clients (22% of NPEs)
External API calls that return null on error states, empty results, or specific response codes. Developers often check the HTTP status code but forget that the response body itself can also be null or that a JSON field that should exist sometimes doesn't:
// ❌ Assumes response body is always present
PaymentResult result = stripeClient.charge(request);
String chargeId = result.getChargeId(); // NPE if result is null or chargeId field missing
This is particularly common with payment APIs, notification services, and any third-party service that has outage modes or partial-response behaviors.
The fix: Treat every external API call as potentially returning null at every level — status, body, and individual fields. Define defensive wrappers:
// ✅ Defensive wrapper with explicit null handling
public Optional<String> charge(PaymentRequest request) {
try {
PaymentResult result = stripeClient.charge(request);
if (result == null) return Optional.empty();
return Optional.ofNullable(result.getChargeId());
} catch (StripeException e) {
log.error("Stripe charge failed", e);
return Optional.empty();
}
}
Pattern 3: Collection methods that silently return null (16% of NPEs)
Several common Java collection and map operations return null rather than throwing an exception or returning an empty collection. The most common offenders:
Map.get(key)returns null for missing keysQueue.peek()andQueue.poll()return null on empty queuesResultSetnavigation methods- JPA
EntityManager.find()returns null for missing entities
// ❌ Map.get() returns null for missing keys
String config = configMap.get("timeout");
int timeout = Integer.parseInt(config); // NPE if key doesn't exist
The fix: Use the null-safe alternatives:
// ✅ getOrDefault prevents the null
String config = configMap.getOrDefault("timeout", "5000");
int timeout = Integer.parseInt(config);
// Or for typed access with validation:
int timeout = Optional.ofNullable(configMap.get("timeout"))
.map(Integer::parseInt)
.orElse(5000);
Pattern 4: Race conditions in initialization (9% of NPEs)
A field is null not because the programmer forgot to initialize it, but because two threads are racing and one thread accesses the field before the other thread has finished initializing it:
// ❌ Non-thread-safe lazy initialization
private Connection connection;
public void query(String sql) {
if (connection == null) {
connection = createConnection(); // Another thread may be here simultaneously
}
connection.execute(sql); // NPE if another thread saw null between check and assign
}
These NPEs are particularly frustrating because they're intermittent — they don't reproduce consistently and often disappear under single-threaded test conditions.
The fix: Use volatile with double-checked locking, use AtomicReference, or use synchronized initialization:
// ✅ Thread-safe initialization using holder pattern
private static class ConnectionHolder {
static final Connection INSTANCE = createConnection();
}
public void query(String sql) {
ConnectionHolder.INSTANCE.execute(sql);
}
Pattern 5: Deserialization of optional JSON fields (6% of NPEs)
A JSON payload omits an optional field, and the deserializer sets the corresponding Java field to null rather than a default value. The consuming code then accesses it without a null check:
// JSON: {"userId": "123"} — no "preferences" field
// Java:
UserPreferences prefs = user.getPreferences();
String theme = prefs.getTheme(); // NPE because prefs is null after deserialization
This pattern is common when APIs evolve — a field that was always present in version 1 becomes optional in version 2, and the Java consumer hasn't been updated.
The fix: Use Jackson's @JsonSetter(nulls = Nulls.AS_EMPTY) or provide default values in the POJO:
// ✅ Default value prevents null
public class User {
private UserPreferences preferences = UserPreferences.defaults();
@JsonSetter(nulls = Nulls.AS_EMPTY)
public void setPreferences(UserPreferences prefs) {
this.preferences = prefs != null ? prefs : UserPreferences.defaults();
}
}
Systematic prevention: three rules for NPE-resistant code
Beyond fixing individual occurrences, the teams whose codebases generate the fewest NPEs in our dataset tend to follow three consistent practices:
Rule 1: Never return null; return Optional or empty collections. Methods that may not find a result should return Optional<T>. Methods that return collections should return an empty collection, not null. This forces callers to handle the absent case explicitly.
Rule 2: Validate at the boundary, trust in the interior. Perform null checks at the entry points of your system — API controllers, message consumers, service method entry points — and annotate internal methods with @NonNull. This concentrates null-handling at the edges and lets the interior code be cleaner.
Rule 3: Use static analysis. NullAway, SpotBugs, and IntelliJ's built-in null analysis can catch the majority of NPE-prone patterns at compile time. Add them to your CI pipeline so they block merges, not just warn during development.
Using ErrorLens on NPEs in practice
When ErrorLens analyzes a stack trace containing an NPE, it traces the null value's origin through the call chain — not just the line where the NPE was thrown. For the most common patterns above, it identifies which of the five root cause types applies and generates a targeted before/after code fix rather than a generic "add a null check" suggestion.
If you're currently dealing with a recurring NPE in a Java application, drop your stack trace into ErrorLens and it will identify which pattern you're dealing with and what the specific fix looks like in your code's context.