Safe Useof Optional Fabric Mod Dependencies via ClassLoader

Summary

During a routine integration test for a new Fabric mod, we encountered a ClassNotFoundError that crashed the client during the bootstrapping phase. The issue arose from an attempt to perform conditional logic based on the presence of a non-mod dependency—specifically a plain Java library (folk.sisby:kaleido-config) that was being loaded via a different classloader scope than the standard Fabric mods.

The failure demonstrates a common pitfall when developers assume that dependency presence in a build file (Gradle) equates to runtime availability within a specific mod’s execution context.

Root Cause

The failure was driven by two primary technical oversights:

  • Classloader Isolation: Fabric uses a specialized classloading system. While a library might be present in the classpath, attempting to reference its classes directly in your mod’s main initialization code triggers a hard dependency at the classloading level.
  • Static Reference Leakage: The developer attempted to check for the library using Class.forName("com.example.LibraryClass"). While this is a valid check, if any part of the mod’s static initialization blocks or field declarations references that class, the JVM will attempt to resolve the class immediately, leading to a crash if the library is missing.

Why This Happens in Real Systems

In complex, modular ecosystems like Minecraft’s Fabric loader, systems are rarely monolithic. This happens because:

  • Transitive Dependency Uncertainty: You might depend on a mod that depends on a library. If the end-user removes the mod but keeps the library (or vice versa), the dependency graph becomes unpredictable.
  • Build-time vs. Runtime Discrepancy: Gradle ensures the library is there for compilation, but it cannot guarantee the library’s presence in the user’s deployment folder unless it is explicitly packaged or required by another loaded mod.
  • Implicit vs. Explicit Dependencies: Developers often confuse implementation (needed to build) with runtimeOnly or provided (expected to be there).

Real-World Impact

  • Environment Instability: A single user’s custom modpack can become unplayable because one mod makes an incorrect assumption about the environment.
  • Bootstrapping Failures: Errors occurring during the onInitialize phase prevent the game from reaching the main menu, making it difficult for users to debug without reading raw logs.
  • Increased Support Burden: Developers spend hours troubleshooting “missing class” errors that are actually logic errors in how the mod handles optional features.

Example or Code

public class DependencyChecker {
    private static boolean IS_LIBRARY_PRESENT = false;

    public static void init() {
        try {
            // Attempt to load a class from the optional library
            Class.forName("folk.sisby.kaleidoconfig.ConfigManager");
            IS_LIBRARY_PRESENT = true;
        } catch (ClassNotFoundException e) {
            IS_LIBRARY_PRESENT = false;
        }
    }

    public static void executeFeature() {
        if (IS_LIBRARY_PRESENT) {
            // This logic is safe because it's wrapped in a conditional 
            // and doesn't call the library in a static context.
            performLibraryLogic();
        }
    }

    private static void performLibraryLogic() {
        // Logic that uses the external library classes
    }
}

How Senior Engineers Fix It

Senior engineers treat all external dependencies as untrusted and volatile. The fix involves:

  • Reflection-Based Probing: Using Class.forName() within a try-catch block, ensuring the check is performed at runtime and not during class loading.
  • Decoupled Logic Paths: Ensuring that no static fields or method signatures in the main mod class reference the external library. This prevents the JVM from attempting to resolve the external class when your mod is loaded.
  • Interface Abstraction: Creating an internal interface for the feature and only implementing the “External Library” version of that interface if the library is successfully detected.
  • Strict Dependency Scoping: Using Gradle’s compileOnly for libraries that are expected to be provided by the environment, preventing the library from being accidentally bundled into the mod JAR.

Why Juniors Miss It

  • The “It Works on My Machine” Fallacy: Juniors often have the library present in their development environment, so the code works perfectly during testing, failing only in the “real world.”
  • Ignoring the Classloader: They treat the Java classpath as a single, flat entity rather than understanding the hierarchical and isolated nature of modern mod loaders.
  • Over-reliance on Static Imports: A junior will often use a static import for a utility class from the external library. This creates a hard link that causes the entire mod to crash during the initial class-loading phase, regardless of any if/else checks later in the code.

Leave a Comment