TestForge Blog
← All Posts

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)
  • final field → 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

  1. Constructor injection (@RequiredArgsConstructor)
  2. Optional.orElseThrow / orElse aggressively
  3. Never return null from collection methods — return empty collections
  4. Handle external data with Optional chains
  5. Annotate with @Nonnull / @Nullable to express intent
  6. 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");
}