1. Exception Handling

Exceptions happen from a user code wether it is intentional or not. This section describes how spring-shell handles exceptions and gives instructions and best practices how to work with it.

Many command line applications when applicable return an exit code which running environment can use to differentiate if command has been executed successfully or not. In a spring-shell this mostly relates when a command is run on a non-interactive mode meaning one command is always executed once with an instance of a spring-shell. Take a note that exit code always relates to non-interactive shell.

1.1. Exception Resolving

Unhandled exceptions will bubble up into shell’s ResultHandlerService and then eventually handled by some instance of ResultHandler. Chain of ExceptionResolver implementations can be used to resolve exceptions and gives you flexibility to return message to get written into console together with exit code which are wrapped within CommandHandlingResult. CommandHandlingResult may contain a message and/or exit code.

static class CustomExceptionResolver implements CommandExceptionResolver {

	@Override
	public CommandHandlingResult resolve(Exception e) {
		if (e instanceof CustomException) {
			return CommandHandlingResult.of("Hi, handled exception\n", 42);
		}
		return null;
	}
}

CommandExceptionResolver implementations can be defined globally as bean.

@Bean
CustomExceptionResolver customExceptionResolver() {
	return new CustomExceptionResolver();
}

or defined per CommandRegistration if it’s applicable only for a particular command itself.

CommandRegistration.builder()
	.withErrorHandling()
		.resolver(new CustomExceptionResolver())
		.and()
	.build();
Resolvers defined with a command are handled before global resolvers.

Use you own exception types which can also be an instance of boot’s ExitCodeGenerator if you want to define exit code there.

static class CustomException extends RuntimeException implements ExitCodeGenerator {

	@Override
	public int getExitCode() {
		return 0;
	}
}

1.2. Exit Code Mappings

Default behaviour of an exit codes is as:

  • Errors from a command option parsing will result code of 2

  • Any generic error will result result code of 1

  • Obviously in any other case result code is 0

Every CommandRegistration can define its own mappings between Exception and exit code. Essentially we’re bound to functionality in Spring Boot regarding exit code and simply integrate into that.

Assuming there is an exception show below which would be thrown from a command:

static class MyException extends RuntimeException {

	private final int code;

	MyException(String msg, int code) {
		super(msg);
		this.code = code;
	}

	public int getCode() {
		return code;
	}
}

It is possible to define a mapping function between Throwable and exit code. You can also just configure a class to exit code which is just a syntactic sugar within configurations.

CommandRegistration.builder()
	.withExitCode()
		.map(MyException.class, 3)
		.map(t -> {
			if (t instanceof MyException) {
				return ((MyException) t).getCode();
			}
			return 0;
		})
		.and()
	.build();
Exit codes cannot be customized with annotation based configuration

1.3. @ExceptionResolver

@ShellComponent classes can have @ExceptionResolver methods to handle exceptions from component methods. These are meant for annotated methods.

The exception may match against a top-level exception being propagated (e.g. a direct IOException being thrown) or against a nested cause within a wrapper exception (e.g. an IOException wrapped inside an IllegalStateException). This can match at arbitrary cause levels.

For matching exception types, preferably declare the target exception as a method argument, as the preceding example(s) shows. When multiple exception methods match, a root exception match is generally preferred to a cause exception match. More specifically, the ExceptionDepthComparator is used to sort exceptions based on their depth from the thrown exception type.

Alternatively, the annotation declaration may narrow the exception types to match, as the following example shows:

@ExceptionResolver({ RuntimeException.class })
CommandHandlingResult errorHandler(Exception e) {
	// Exception would be type of RuntimeException,
	// optionally do something with it
	return CommandHandlingResult.of("Hi, handled exception\n", 42);
}
@ExceptionResolver
CommandHandlingResult errorHandler(RuntimeException e) {
	return CommandHandlingResult.of("Hi, handled custom exception\n", 42);
}

@ExceptionResolver can also return String which is used as an output to console. You can use @ExitCode annotation to define return code.

@ExceptionResolver
@ExitCode(code = 5)
String errorHandler(Exception e) {
	return "Hi, handled exception";
}

@ExceptionResolver with void return type is automatically handled as handled exception. You can then also define @ExitCode and use Terminal if you need to write something into console.

@ExceptionResolver
@ExitCode(code = 5)
void errorHandler(Exception e, Terminal terminal) {
	PrintWriter writer = terminal.writer();
	String msg =  "Hi, handled exception " + e.toString();
	writer.println(msg);
	writer.flush();
}
Method Arguments

@ExceptionResolver methods support the following arguments:

Method argument Description

Exception type

For access to the raised exception. This is any type of Exception or Throwable.

Terminal

For access to underlying JLine terminal to i.e. get its terminal writer.

Return Values

@ExceptionResolver methods support the following return values:

Return value Description

String

Plain text to return to a shell. Exit code 1 is used in this case.

CommandHandlingResult

Plain CommandHandlingResult having message and exit code.

void

A method with a void return type is considered to have fully handled the exception. Usually you would define Terminal as a method argument and write response using terminal writer from it. As exception is fully handled, Exit code 0 is used in this case.