Unreleased version v6.3.0-SNAPSHOT. This page describes the alpha branch and may change at any time; it is not part of any released version.

commit 49619c6 · 2026-09-13 12:23 UTC

Skip to content

Exception Handling

Since v6.2.0

Automatic exception catching and handling for service methods.

UltiTools provides declarative exception handling through the @ExceptionCatch annotation. Instead of wrapping service method calls in try-catch blocks, you simply annotate a method and the framework handles exceptions automatically based on your configuration.

@ExceptionCatch has no reader in v6.2.5

The aop package is connected to the rest of the framework only by two javadoc references in v6.2.5: no proxy is created, no advisor is registered and ExceptionInterceptor is never instantiated, so an annotated method throws exactly as it would without the annotation and silent, value, defaultValue and handler all stay inert. Wrap the call in an ordinary try-catch until the wiring ships: everything described on this page, including the handler lookup by name further down, depends on that one missing connection. The wiring is merged into the development branch but is not part of v6.2.5; it is tracked in issue #190.

Basic Usage

Add @ExceptionCatch to any method inside a managed bean (such as a @Service):

java
package com.ultikits.docs.exception;

import com.ultikits.ultitools.abstracts.UltiToolsPlugin;
import com.ultikits.ultitools.annotations.*;
import com.ultikits.ultitools.aop.ExceptionHandler;
import com.ultikits.ultitools.interfaces.DataOperator;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

@Service
public class FileService {

    @ExceptionCatch
    // @ExceptionCatch is runtime AOP. It catches the exception when the method
    // is invoked through the proxy, but javac still requires a checked exception
    // to be declared, so `throws IOException` is not optional here.
    public String readFile(String path) throws IOException {
        // If any exception occurs, it will be caught and logged
        // The method returns null
        return new String(Files.readAllBytes(Paths.get(path)));
    }
}

By default:

  • All Exception types are caught
  • Exceptions are logged as warnings (unless silent = true)
  • A default value (null for objects, 0 for primitives) is returned

Annotation Attributes

AttributeTypeDefaultDescription
valueClass<? extends Throwable>[]{Exception.class}Exception types to catch. Subclasses are automatically included.
silentbooleanfalseIf true, exceptions are caught without logging. If false, caught exceptions are logged as warnings. Either way, the exception is also reported to the framework's ErrorReportCollector.
handlerString""Name of a custom exception handler bean. The bean must implement ExceptionHandler.
defaultValueString""Expression specifying the return value when an exception is caught.

Catching Specific Exceptions

Specify which exception types should be caught:

java
package com.ultikits.docs.exception;

import com.ultikits.ultitools.abstracts.UltiToolsPlugin;
import com.ultikits.ultitools.annotations.*;
import com.ultikits.ultitools.aop.ExceptionHandler;
import com.ultikits.ultitools.interfaces.DataOperator;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

@Service
public class DataService {

    @ExceptionCatch(IOException.class)
    public String loadData() {
        // Only IOException will be caught
        // Other exceptions will propagate up
        return readFromFile();
    }

    @ExceptionCatch({IOException.class, SQLException.class})
    public List<User> fetchUsers() {
        // Both IOException and SQLException will be caught
        // Subclasses are also caught
        return queryDatabase();
    }

    private String readFromFile() { return ""; }

    private List<User> queryDatabase() { return new ArrayList<>(); }
}

Exception Hierarchy

When you specify an exception type, the framework also catches its subclasses. For example, @ExceptionCatch(IOException.class) will catch FileNotFoundException, EOFException, and other subclasses of IOException.

Silent Mode

Suppress logging for expected or non-critical exceptions:

java
package com.ultikits.docs.exception;

import com.ultikits.ultitools.abstracts.UltiToolsPlugin;
import com.ultikits.ultitools.annotations.*;
import com.ultikits.ultitools.aop.ExceptionHandler;
import com.ultikits.ultitools.interfaces.DataOperator;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

@Service
public class ConfigService {

    @ExceptionCatch(silent = true)
    public void saveOptionalConfig() {
        // Any exception is caught and NOT logged
        // Useful for non-critical background operations
        writeConfigBackup();
    }

    @ExceptionCatch(value = FileNotFoundException.class, silent = true)
    public boolean fileExists(String path) {
        // FileNotFoundException is silently caught
        // Other exceptions propagate up uncaught (not caught, not logged)
        return checkFile(path);
    }

    private void writeConfigBackup() { }

    private boolean checkFile(String path) { return true; }
}

Use silent = true for:

  • Non-critical operations (e.g., optional backups)
  • Fallback logic (e.g., use default if file not found)
  • Operations where exceptions are expected

Default Return Values

Control what value is returned when an exception is caught:

java
package com.ultikits.docs.exception;

import com.ultikits.ultitools.abstracts.UltiToolsPlugin;
import com.ultikits.ultitools.annotations.*;
import com.ultikits.ultitools.aop.ExceptionHandler;
import com.ultikits.ultitools.interfaces.DataOperator;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

@Service
public class MoneyService {

    @ExceptionCatch(defaultValue = "0")
    public int getBalance(String accountId) {
        // If exception occurs, returns 0 instead of null
        return queryBalance(accountId);
    }

    @ExceptionCatch(defaultValue = "false")
    public boolean isPlayerOnline(String playerName) {
        // Returns false instead of null
        return checkDatabase(playerName);
    }

    @ExceptionCatch(defaultValue = "empty")
    public List<User> getAllUsers() {
        // Returns empty list instead of null
        return queryAllUsers();
    }

    private int queryBalance(String accountId) { return 0; }

    private boolean checkDatabase(String playerName) { return false; }

    private List<User> queryAllUsers() { return new ArrayList<>(); }
}

Supported default value expressions:

  • "null" — returns null (default for objects)
  • "true" / "false" — returns boolean
  • Numeric literals — "0", "100", "-5", "3.14" — returns the number
  • "empty" — returns empty collection/array/string based on return type

If defaultValue is not specified, a type-appropriate default is used:

  • Objects: null
  • boolean: false
  • int, long, etc.: 0
  • String: null
  • Collections: null

defaultValue Type Matching

The defaultValue expression is parsed according to the method's return type. If you specify defaultValue = "0" on a String-returning method, it returns the string "0", not the number zero.

Custom Exception Handlers

Implement custom logic for exception handling by creating an ExceptionHandler bean:

java
package com.ultikits.docs.exception;

import com.ultikits.ultitools.abstracts.UltiToolsPlugin;
import com.ultikits.ultitools.annotations.*;
import com.ultikits.ultitools.aop.ExceptionHandler;
import com.ultikits.ultitools.interfaces.DataOperator;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

@Service
public class LoggingExceptionHandler implements ExceptionHandler {

    @Override
    public Object handleException(Throwable exception, Object target, Method method, Object[] args) {
        // Log detailed exception information
        System.out.println("Exception in: " + method.getDeclaringClass().getSimpleName() + "." + method.getName());
        System.out.println("Message: " + exception.getMessage());
        exception.printStackTrace();
        return null;
    }

    @Override
    public boolean supports(Class<? extends Throwable> exceptionType) {
        // This handler supports any exception
        return true;
    }
}

Register the handler and reference it by name:

java
package com.ultikits.docs.exception;

import com.ultikits.ultitools.abstracts.UltiToolsPlugin;
import com.ultikits.ultitools.annotations.*;
import com.ultikits.ultitools.aop.ExceptionHandler;
import com.ultikits.ultitools.interfaces.DataOperator;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

@Service
public class MyService {

    @ExceptionCatch(handler = "loggingExceptionHandler")
    public String processData() {
        // If an exception occurs, LoggingExceptionHandler.handleException() is called
        return getData();
    }

    private String getData() { return ""; }
}

Handler Interface

Custom handlers implement the ExceptionHandler interface, whose handleException(Throwable, Object, Method, Object[]) method holds the main logic and can return a replacement value or re-throw. supports(Class) is optional and reports whether the handler covers a given exception type, defaulting to true for all types. getOrder() is optional too and sets priority, where lower values run first and the default is 0.

Method Requirements

@ExceptionCatch works only on methods in beans managed by the IoC container:

java
@Service
public class MyService {

    @ExceptionCatch  // CORRECT - method in a managed @Service bean
    public void safeOperation() {
        // ...
    }
}

public class NonManagedClass {

    @ExceptionCatch  // WRONG - this class is not a bean
    public void unsafeOperation() {
        // The annotation has no effect
    }
}

Supported bean types:

  • @Service — services
  • @Component — general-purpose beans
  • Any class registered manually in the IoC container

Complete Example

java
package com.ultikits.docs.exception;

import com.ultikits.ultitools.abstracts.UltiToolsPlugin;
import com.ultikits.ultitools.annotations.*;
import com.ultikits.ultitools.aop.ExceptionHandler;
import com.ultikits.ultitools.interfaces.DataOperator;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

@Service
public class UserDatabaseService {

    @Autowired
    private UltiToolsPlugin plugin;

    // Safe read: returns null on any exception, with logging
    @ExceptionCatch
    public User findById(String userId) {
        DataOperator<User> op = plugin.getDataOperator(User.class);
        return op.query().where("id").eq(userId).first();
    }

    // Safe read with default: returns empty list if query fails
    @ExceptionCatch(defaultValue = "empty")
    public List<User> findByRole(String role) {
        DataOperator<User> op = plugin.getDataOperator(User.class);
        return op.query().where("role").eq(role).list();
    }

    // Safe with silent mode: no logging for file-not-found
    @ExceptionCatch(value = FileNotFoundException.class, silent = true)
    public String loadUserData(String filename) {
        return readFile(filename);
    }

    // Safe with custom handler: detailed error reporting
    @ExceptionCatch(
        value = {SQLException.class, IOException.class},
        handler = "detailedErrorHandler",
        defaultValue = "null"
    )
    public String exportUsers() {
        // If SQLException or IOException occurs, detailedErrorHandler is invoked
        return performExport();
    }

    // Critical operation: no exception catching, propagates up
    public void deleteUser(String userId) {
        // No @ExceptionCatch - exceptions must be handled by caller
        DataOperator<User> op = plugin.getDataOperator(User.class);
        op.query().where("id").eq(userId).delete();
    }

    private String readFile(String filename) { return ""; }

    private String performExport() { return ""; }
}

Best Practices

Custom handlers work best for fault tolerance, catching exceptions in methods where failures are expected or non-critical. Specify exception types such as @ExceptionCatch(IOException.class) instead of catching everything, and keep silent = false unless you have a specific reason to suppress the log. Provide meaningful defaults, for example defaultValue = "empty" for collections and "0" for counts, and combine @ExceptionCatch with @Service beans designed for fault tolerance.

See Also

Contributors

No contributors

Released under the MIT License.