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
- Start Gradually
- Enable a few checks first, then add more
- Use
WARNlevel beforeERROR
- Team Adoption
- Educate team about Error Prone benefits
- Create custom checks for team-specific patterns
- CI Integration
- Fail builds on Error Prone errors
- Use in pull request checks
- Suppression Strategy
- Use
@SuppressWarningssparingly - Document why suppression is needed
- Custom Checks
- Create team-specific checks
- Share custom checks across projects
Common Error Prone Bug Patterns
| Category | Bug Pattern | Description |
|---|---|---|
| Equality | ArrayEquals, StringEquals | Incorrect reference comparisons |
| Annotations | MissingOverride | Missing @Override annotations |
| Collections | MutableConstant | Mutable objects used as constants |
| Boxing | BoxedPrimitiveConstructor | Deprecated boxed type constructors |
| Resources | StreamResourceLeak | Unclosed streams and resources |
| Concurrency | FutureReturnValueIgnored | Ignoring Future results |
| Nullness | NullTernary | Suspicious 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.