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.

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.
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 with:

  • handleException(Throwable, Object, Method, Object[]) — main handler logic, returns a replacement value or can re-throw
  • supports(Class) — optional; returns true if this handler supports the exception type (default: true for all)
  • getOrder() — optional; lower values have higher priority (default: 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

  1. Use for fault tolerance — Catch exceptions in methods where failures are expected or non-critical
  2. Specify exception types — Use @ExceptionCatch(IOException.class) instead of catching all exceptions
  3. Enable logging — Keep silent = false unless you have a specific reason to suppress logs
  4. Provide meaningful defaults — Use defaultValue = "empty" for collections, "0" for counts, etc.
  5. Combine with services@ExceptionCatch works best on @Service beans designed for fault tolerance

See Also

Contributors

The avatar of contributor named as Ling Bao Ling Bao

Changelog

Released under the MIT License.