Spring Boot NullPointerException — Root Causes and Prevention Patterns
Seven common causes of NPE in Spring Boot development, and how to prevent them fundamentally using Optional, defensive coding, and tests.
TestForge Team ·
Why NPE Is Dangerous
java.lang.NullPointerException: Cannot invoke
"com.example.UserService.getUser(Long)" because "this.userService" is null
The stack trace alone rarely tells you why something is null.
Java 14+ Helpful NPE improved the messages, but prevention is always better.
Cause 1: @Autowired Field Injection + Direct Constructor Call
// Dangerous
@Service
public class OrderService {
@Autowired
private UserService userService; // Field injection
// If test creates new OrderService(), userService = null
}
// Safe: Constructor injection
@Service
@RequiredArgsConstructor // Lombok
public class OrderService {
private final UserService userService; // Cannot be null
}
Constructor injection benefits:
- Dependencies are explicit
- Tests can do
new OrderService(mockUserService) finalfield → NPE eliminated at the source
Cause 2: Ignoring Optional from JPA
// Dangerous
User user = userRepository.findById(id).get(); // NoSuchElementException if absent
// or
User user = userRepository.findByEmail(email); // May return null
// Safe
User user = userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("User not found: " + id));
// or null-safe handling
Optional<User> optUser = userRepository.findByEmail(email);
optUser.ifPresent(u -> sendWelcomeEmail(u.getEmail()));
Cause 3: Not Checking Map.get() Result
Map<String, Config> configMap = getConfigMap();
// Dangerous
String value = configMap.get("key").getValue(); // null.getValue() → NPE
// Safe
Config config = configMap.get("key");
if (config != null) {
String value = config.getValue();
}
// or
String value = Optional.ofNullable(configMap.get("key"))
.map(Config::getValue)
.orElse("default");
// or
Config config = configMap.getOrDefault("key", Config.defaultConfig());
Cause 4: Assuming External API Fields Are Present
// Dangerous
ExternalResponse response = apiClient.call();
String name = response.getData().getUser().getName(); // Chain NPE
// Safe
String name = Optional.ofNullable(response)
.map(ExternalResponse::getData)
.map(Data::getUser)
.map(User::getName)
.orElse("Unknown");
Cause 5: Returning Null from Collection Methods
// Dangerous: returning null
public List<Order> getOrders(Long userId) {
if (!userExists(userId)) return null; // Callers who don't check → NPE
// ...
}
// Safe: return empty collection
public List<Order> getOrders(Long userId) {
if (!userExists(userId)) return Collections.emptyList();
// ...
}
Rule: Methods returning collections must never return null.
Cause 6: @Value Injection Failure
@Value("${app.secret}")
private String secret;
// Missing property → BeanCreationException (not null, but a crash)
// Missing default value → production incident
// Safe: default value or explicit validation
@Value("${app.secret:}")
private String secret; // Empty string if missing
// For required values, validate with @NotBlank
@NotBlank
@Value("${app.secret}")
private String secret;
Cause 7: @RequestParam / @PathVariable Handling
// Dangerous
@GetMapping("/users")
public ResponseEntity<?> getUsers(@RequestParam String email) {
User user = userService.findByEmail(email);
return ResponseEntity.ok(user.toDto()); // NPE if user is null
}
// Safe
@GetMapping("/users")
public ResponseEntity<?> getUsers(@RequestParam String email) {
return userService.findByEmail(email)
.map(user -> ResponseEntity.ok(user.toDto()))
.orElse(ResponseEntity.notFound().build());
}
Prevention: Static Analysis Tools
<!-- SpotBugs annotations -->
<dependency>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-annotations</artifactId>
<scope>provided</scope>
</dependency>
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
public class UserService {
@Nonnull
public User getUser(@Nonnull Long id) { ... }
@Nullable
public User findByEmail(@Nonnull String email) { ... }
}
IntelliJ IDEA will warn when a @Nullable return value is used without a null check.
Quick Prevention Rules
- Constructor injection (
@RequiredArgsConstructor) - Optional.orElseThrow / orElse aggressively
- Never return null from collection methods — return empty collections
- Handle external data with Optional chains
- Annotate with @Nonnull / @Nullable to express intent
- Always include null-case scenarios in unit tests
// Test example
@Test
void getUser_notFound_throwsException() {
when(userRepository.findById(999L)).thenReturn(Optional.empty());
assertThatThrownBy(() -> userService.getUser(999L))
.isInstanceOf(UserNotFoundException.class)
.hasMessageContaining("999");
}