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.