Error Prone: Catch Bugs at Compile Time in Java

Error Prone is a static analysis tool for Java that catches common programming mistakes at compile time. It extends the Java compiler to identify bugs, anti-patterns, and suspicious code constructs that regular compilers might miss.


What is Error Prone?

Error Prone is a Java compiler plugin that performs additional compile-time checks beyond standard Java compilation. It was developed at Google and is used extensively in their Java codebase to prevent bugs before they reach production.

Key Benefits:

  • Compile-time bug detection - Catch issues before runtime
  • Zero runtime overhead - Analysis happens during compilation
  • Extensible - Create custom checks
  • IDE integration - Works with popular IDEs
  • Build tool support - Maven, Gradle, Bazel

How Error Prone Works

Java Source → javac + Error Prone → Compilation Errors/Warnings
↓
Additional Bug Checks

Getting Started

Maven Configuration

<project>
<properties>
<errorprone.version>2.18.0</errorprone.version>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>11</source>
<target>11</target>
<encoding>UTF-8</encoding>
<compilerArgs>
<arg>-XDcompilePolicy=simple</arg>
<arg>-Xplugin:ErrorProne</arg>
</compilerArgs>
<annotationProcessorPaths>
<path>
<groupId>com.google.errorprone</groupId>
<artifactId>error_prone_core</artifactId>
<version>${errorprone.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<!-- Error Prone annotations -->
<dependency>
<groupId>com.google.errorprone</groupId>
<artifactId>error_prone_annotations</artifactId>
<version>${errorprone.version}</version>
</dependency>
</dependencies>
</project>

Gradle Configuration

plugins {
id 'java'
id 'net.ltgt.errorprone' version '3.0.1'
}
repositories {
mavenCentral()
}
dependencies {
errorprone 'com.google.errorprone:error_prone_core:2.18.0'
implementation 'com.google.errorprone:error_prone_annotations:2.18.0'
}
tasks.withType(JavaCompile) {
options.compilerArgs += [
'-Xplugin:ErrorProne',
'-XDcompilePolicy=simple'
]
}

Common Error Prone Checks with Examples

1. ArrayEquals - Incorrect array comparison

import java.util.Arrays;
public class ArrayEqualsBug {
public static void main(String[] args) {
int[] a = {1, 2, 3};
int[] b = {1, 2, 3};
// BUG: Using == for array comparison
if (a == b) {  // Error Prone will catch this!
System.out.println("Arrays are equal");
}
// CORRECT: Use Arrays.equals()
if (Arrays.equals(a, b)) {
System.out.println("Arrays are equal");
}
}
}

Error Prone Output:

error: [ArrayEquals] Reference equality used to compare arrays
if (a == b) {
^
(see https://errorprone.info/bugpattern/ArrayEquals)

2. StringEquals - Incorrect string comparison

public class StringEqualsBug {
public boolean checkString(String input) {
// BUG: Using == for string comparison
return input == "expected";  // Error Prone will catch this!
}
public boolean checkStringCorrect(String input) {
// CORRECT: Use equals()
return "expected".equals(input);
}
public boolean checkNullableString(String input) {
// BUG: Potential NPE
return input.equals("expected");  // Error Prone suggests better pattern
// CORRECT: Put the literal first
// return "expected".equals(input);
}
}

3. MissingOverride - Missing @Override annotation

interface Animal {
void makeSound();
String getName();
}
public class Dog implements Animal {
// BUG: Missing @Override annotation
public void makeSound() {  // Error Prone will warn
System.out.println("Woof!");
}
// CORRECT: With @Override
@Override
public String getName() {
return "Dog";
}
}
class BaseClass {
public void importantMethod() {
System.out.println("Base implementation");
}
}
class DerivedClass extends BaseClass {
// BUG: Typo in method name, but no compilation error
public void importantMethod() {  // Error Prone catches the missing @Override
System.out.println("Derived implementation");
}
}

4. MutableConstant - Mutable objects as constants

import java.util.Date;
import java.util.ArrayList;
import java.util.List;
public class MutableConstants {
// BUG: Date is mutable
public static final Date CREATION_DATE = new Date();  // Error Prone warning
// BUG: ArrayList is mutable
public static final List<String> NAMES = new ArrayList<>();  // Error Prone warning
// CORRECT: Use immutable alternatives
public static final String CREATION_DATE_STRING = "2024-01-01";
public static final List<String> IMMUTABLE_NAMES = List.of("Alice", "Bob");
}

5. BoxedPrimitiveConstructor - Deprecated boxed primitive constructors

public class BoxedPrimitiveBug {
public void problematicCode() {
// BUG: Using deprecated constructors
Integer number = new Integer(42);  // Error Prone warning
Boolean flag = new Boolean(true);  // Error Prone warning
Long bigNumber = new Long(123L);   // Error Prone warning
// CORRECT: Use valueOf() or autoboxing
Integer goodNumber = Integer.valueOf(42);
Boolean goodFlag = Boolean.TRUE;
Long goodBigNumber = 123L;  // autoboxing
}
}

6. DeadException - Exceptions that are immediately thrown

public class DeadExceptionExample {
public void validateInput(String input) {
// BUG: Exception created and immediately thrown
if (input == null) {
throw new IllegalArgumentException("Input cannot be null");  // Error Prone may warn about style
}
// BUG: Unnecessarily creating exception objects
IllegalArgumentException ex = new IllegalArgumentException();
if (input.isEmpty()) {
throw ex;  // Better to create when needed
}
}
// CORRECT: Create exception only when needed
public void betterValidateInput(String input) {
if (input == null) {
throw new IllegalArgumentException("Input cannot be null");
}
if (input.isEmpty()) {
throw new IllegalArgumentException("Input cannot be empty");
}
}
}

7. FutureReturnValueIgnored - Ignoring Future return values

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class FutureIgnoredBug {
private final ExecutorService executor = Executors.newSingleThreadExecutor();
public void processTask() {
// BUG: Ignoring Future return value
executor.submit(() -> doWork());  // Error Prone warning
// CORRECT: Handle the Future
Future<?> future = executor.submit(() -> doWork());
// Can now check cancellation, exceptions, etc.
}
private void doWork() {
// Some long-running task
}
}

8. StreamResource - Unclosed streams

import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
public class StreamResourceBug {
public void readFileProblematic(String filename) throws IOException {
// BUG: Stream not closed in all paths
FileInputStream fis = new FileInputStream(filename);  // Error Prone warning
if (someCondition()) {
return;  // Stream leaked!
}
fis.close();
}
public void readFileCorrect(String filename) throws IOException {
// CORRECT: Use try-with-resources
try (FileInputStream fis = new FileInputStream(filename)) {
// Use the stream
}
}
public void modernJavaRead(String filename) throws IOException {
// CORRECT: Using Files API
Files.readAllBytes(Paths.get(filename));
}
private boolean someCondition() {
return Math.random() > 0.5;
}
}

Advanced Error Prone Usage

Custom Error Prone Checks

import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.matchers.Matchers;
import com.sun.source.tree.MethodTree;
// Custom Error Prone check to enforce naming conventions
@BugPattern(
name = "LoggerNamingConvention",
summary = "Logger instances should be named 'log' or 'logger'",
severity = BugPattern.SeverityLevel.WARNING,
tags = BugPattern.StandardTags.STYLE
)
public class LoggerNamingCheck extends BugChecker implements MethodTreeMatcher {
private static final Matcher<MethodTree> LOGGER_FIELD = Matchers.isField()
.and(Matchers.isSubtypeOf("org.slf4j.Logger"))
.and(Matchers.not(Matchers.anyOf(
Matchers.hasName("log"),
Matchers.hasName("logger")
)));
@Override
public Description matchMethod(MethodTree tree, VisitorState state) {
if (LOGGER_FIELD.matches(tree, state)) {
return describeMatch(tree);
}
return Description.NO_MATCH;
}
}

Using Error Prone Annotations

import com.google.errorprone.annotations.Immutable;
import com.google.errorprone.annotations.CheckReturnValue;
import com.google.errorprone.annotations.Var;
@Immutable  // Error Prone will check that all fields are final
public final class ImmutablePoint {
private final double x;
private final double y;
public ImmutablePoint(double x, double y) {
this.x = x;
this.y = y;
}
@CheckReturnValue  // Error if return value is ignored
public ImmutablePoint translate(double dx, double dy) {
return new ImmutablePoint(x + dx, y + dy);
}
public void process() {
@Var int count = 0;  // @Var documents that variable is mutated
count++;
System.out.println(count);
}
}

Configuration and Customization

Error Prone Flags Configuration

Maven configuration with custom flags:

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>11</source>
<target>11</target>
<compilerArgs>
<arg>-XDcompilePolicy=simple</arg>
<arg>-Xplugin:ErrorProne -Xep:StringEquals:WARN -Xep:MissingOverride:ERROR -Xep:DeadException:OFF</arg>
</compilerArgs>
<annotationProcessorPaths>
<path>
<groupId>com.google.errorprone</groupId>
<artifactId>error_prone_core</artifactId>
<version>2.18.0</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>

Error Prone Suppression

import com.google.errorprone.annotations.SuppressWarnings;
public class SuppressionExample {
@SuppressWarnings("ArrayEquals")  // Suppress specific check
public boolean compareArrays(int[] a, int[] b) {
// We have a good reason to use reference equality here
return a == b;
}
@SuppressWarnings({"StringEquals", "MissingOverride"})  // Suppress multiple checks
public void multipleSuppressions() {
String s = "test";
if (s == "test") {  // Intentional reference comparison
// ...
}
}
// Use standard @SuppressWarnings for some checks
@java.lang.SuppressWarnings("FutureReturnValueIgnored")
public void ignoreFuture() {
executor.submit(() -> doWork());  // We don't care about the Future
}
}

Integration with Build Tools

GitHub Actions with Error Prone

name: Java CI with Error Prone
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 11
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'temurin'
- name: Build with Maven and Error Prone
run: mvn compile -X
- name: Run Error Prone explicitly
run: mvn compile -Dmaven.compiler.failOnWarning=true

Real-World Examples

Example: Comprehensive Error Prone Usage

import com.google.errorprone.annotations.CheckReturnValue;
import com.google.errorprone.annotations.Immutable;
import java.util.List;
import java.util.Objects;
@Immutable
public final class Person {
private final String name;
private final int age;
private final List<String> emails;
public Person(String name, int age, List<String> emails) {
this.name = Objects.requireNonNull(name, "Name cannot be null");
this.age = age;
this.emails = List.copyOf(Objects.requireNonNull(emails, "Emails cannot be null"));
}
@CheckReturnValue
public Person withName(String newName) {
return new Person(newName, age, emails);
}
@CheckReturnValue
public Person withAge(int newAge) {
return new Person(name, newAge, emails);
}
// Proper equals and hashCode
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Person)) return false;
Person person = (Person) o;
return age == person.age && 
name.equals(person.name) && 
emails.equals(person.emails);
}
@Override
public int hashCode() {
return Objects.hash(name, age, emails);
}
// Proper toString
@Override
public String toString() {
return String.format("Person{name='%s', age=%d, emails=%s}", name, age, emails);
}
}

Best Practices

  1. Start Gradually
  • Enable a few checks first, then add more
  • Use WARN level before ERROR
  1. Team Adoption
  • Educate team about Error Prone benefits
  • Create custom checks for team-specific patterns
  1. CI Integration
  • Fail builds on Error Prone errors
  • Use in pull request checks
  1. Suppression Strategy
  • Use @SuppressWarnings sparingly
  • Document why suppression is needed
  1. Custom Checks
  • Create team-specific checks
  • Share custom checks across projects

Common Error Prone Bug Patterns

CategoryBug PatternDescription
EqualityArrayEquals, StringEqualsIncorrect reference comparisons
AnnotationsMissingOverrideMissing @Override annotations
CollectionsMutableConstantMutable objects used as constants
BoxingBoxedPrimitiveConstructorDeprecated boxed type constructors
ResourcesStreamResourceLeakUnclosed streams and resources
ConcurrencyFutureReturnValueIgnoredIgnoring Future results
NullnessNullTernarySuspicious null checks

Conclusion

Error Prone is a powerful tool that significantly improves Java code quality by:

Key Benefits:

  • Early Bug Detection - Catch issues at compile time
  • Code Quality - Enforce best practices automatically
  • Team Consistency - Standardize code patterns
  • Reduced Debugging - Fewer runtime exceptions
  • Educational - Learn better coding practices

When to Use Error Prone:

  • Large codebases needing consistency
  • Teams wanting to enforce coding standards
  • Projects with strict quality requirements
  • Continuous integration pipelines

Integration Points:

  • Compile-time - As part of regular compilation
  • IDEs - Plugin for immediate feedback
  • CI/CD - Fail builds on errors
  • Code Review - Pre-commit checks

By integrating Error Prone into your development workflow, you can catch many common Java bugs before they ever reach production, saving time and improving code reliability.


Next Steps: Start by enabling a few critical checks in your build, gradually add more patterns as your team becomes comfortable, and consider creating custom checks for your specific domain patterns and team conventions.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper