Skip to main content

Generate Client Errors

This part is actually unrelated to ORM.

However, since generate client api is provided, this part of the functionality must also be provided to make it a complete solution.

Exception Families

We need to define special Java/Kotlin business exceptions that can be directly translated into data that the client can understand.

How do we define this special type of business exception? There are two extreme choices:

  • Share one business exception class across the entire application, with built-in error codes that the client can understand

  • Define a business exception class for each business error

Obviously, both of the above solutions are unsuitable. The first option is too coarse-grained, with globally maintained error codes that are difficult to maintain. The second option is too fine-grained, requiring the definition of too many exception classes.

So, Jimmer chooses a compromise: dividing business exceptions into multiple families, with each family using one set of Error Codes.

Jimmer uses enums to define exception families, where the enum also serves as the error code for that family, e.g.

Define Exception Families

UserInfoErrorCode.java
package com.example.business.error;

import org.babyfish.jimmer.error.ErrorFamily;

@ErrorFamily
public enum UserInfoErrorCode {
ILLEGAL_USER_NAME,
PASSWORD_TOO_SHORT,
PASSWORDS_NOT_SAME
}

The @org.babyfish.jimmer.error.ErrorFamily annotation indicates that this enum represents a family of business exceptions, and the enum itself serves as the error code for this family.

The @ErrorFamily annotation will be processed by Jimmer's precompiler

info

Here, the precompiler refers to Annotation Processor for Java, and KSP for Kotlin.

This part of the code has been discussed in Overview/Get Started/Generate Code, so it won't be repeated here.

The precompiler will generate the following exception class based on this enum:

info

For enums used to declare exception families, you can choose to use ErrorCode or Error as a special suffix.

  • If there is such a special suffix, the exception class name = enum name without the suffix + Exception

  • Otherwise, exception class name = enum name + Exception

So here the generated exception name is UserInfoException

After compilation by Jimmer, the following exception class will be generated.

UserInfoException.java
public abstract class UserInfoException
extends CodeBasedException {

private UserInfoException(String message, Throwable cause) {
super(message, cause);
}

@Override
public abstract UserInfoErrorCode getCode();

public static UserInfoException illegalUserName(@NotNull String message) {
return new IllegalUserName(
message,
null
);
}

public static UserInfoException illegalUserName(
@NotNull String message,
@Nullable Throwable cause
) {
return new IllegalUserName(
message,
cause
);
}

public static UserInfoException passwordTooShort(@NotNull String message) {
return new PasswordTooShort(
message,
null
);
}

public static UserInfoException passwordTooShort(
@NotNull String message,
@Nullable Throwable cause
) {
return new PasswordTooShort(
message,
cause
);
}

public static UserInfoException passwordsNotSame(@NotNull String message) {
return new PasswordsNotSame(
message,
null
);
}

public static UserInfoException passwordsNotSame(
@NotNull String message,
@Nullable Throwable cause
) {
return new PasswordsNotSame(
message,
cause
);
}

public static class IllegalUserName extends UserInfoException {
public IllegalUserName(String message, Throwable cause) {
super(message, cause);
}

@Override
public UserInfoErrorCode getCode() {
return UserInfoErrorCode.ILLEGAL_USER_NAME;
}

@Override
public Map<String, Object> getFields() {
return Collections.emptyMap();
}
}

public static class PasswordTooShort extends UserInfoException {
public PasswordTooShort(String message, Throwable cause) {
super(message, cause);
}

@Override
public UserInfoErrorCode getCode() {
return UserInfoErrorCode.PASSWORD_TOO_SHORT;
}

@Override
public Map<String, Object> getFields() {
return Collections.emptyMap();
}
}

public static class PasswordsNotSame extends UserInfoException {
public PasswordsNotSame(String message, Throwable cause) {
super(message, cause);
}

@Override
public UserInfoErrorCode getCode() {
return UserInfoErrorCode.PASSWORDS_NOT_SAME;
}

@Override
public Map<String, Object> getFields() {
return Collections.emptyMap();
}
}
}
  • ❶ Exceptions based on enum error codes must inherit from org.babyfish.jimmer.error.CodeBasedException

  • ❷ The error code type for this family of exceptions is UserInfoErrorCode

  • ❸❹ Static method to construct exception with error code ILLEGAL_USER_NAME

  • ❺❻ Static method to construct exception with error code PASSWORD_TOO_SHORT

  • ❼❽ Static method to construct exception with error code PASSWORDS_NOT_SAME

  • ❾ Exception class UserInfoException.IllegalUserName has error code ILLEGAL_USER_NAME

  • ❿ Exception class UserInfoException.PasswordTooShort has error code PASSWORD_TOO_SHORT

  • ⓫ Exception class UserInfoException.PasswordsNotSame has error code PASSWORDS_NOT_SAME

Add Fields to Error Codes

Additional fields can be added to any error code.

For example, ILLEGAL_USER_NAME indicates an illegal username, i.e. the username contains illegal characters. We can add the field illegalChars for it.

UserInfoErrorCode.java
@ErrorFamily
public enum UserInfoErrorCode {

@ErrorField(name = "illegalChars", type = Character.class, list = true)
ILLEGAL_USER_NAME,

PASSWORD_TOO_SHORT,

PASSWORDS_NOT_SAME
}

The generated code is as follows:

UserInfoException.java
public abstract class UserInfoException extends CodeBasedException {

public static UserInfoException illegalUserName(
@NotNull String message,
@NotNull List<Character> illegalChars
) {
...omit code...
}

public static UserInfoException illegalUserName(
@NotNull String message,
@Nullable Throwable cause,
@NotNull List<Character> illegalChars
) {
...omit code...
}

public static class IllegalUserName extends UserInfoException {

@NotNull
private final List<Character> illegalChars;

public IllegalUserName(
String message,
Throwable cause,
@NotNull List<Character> illegalChars
) {
super(message, cause);
this.illegalChars = illegalChars;
}

@Override
public Map<String, Object> getFields() {
return Collections.singletonMap("illegalChars", illegalChars);
}

@NotNull
public List<Character> getIllegalChars() {
return illegalChars;
}

...omit other code...
}

...omit other code...
}

Declare Exceptions for REST APIs

Allow To Throw All Exceptions Of Family

UserController.java
package com.example.service;

import org.babyfish.jimmer.client.ThrowsAll;

@PostMapping("/user")
@ThrowsAll(UserInfoErrorCode.class)
public void registerUser(@RequestBody RegisterUserInput input) {
...omit code...
}
info

The @org.babyfish.jimmer.client.ThrowsAll annotation allows the REST API to throw all exceptions in the family.

Allow To Throw Some Exceptions Of Family

Let's locate another business exception family:

PlatformErrorCode.java
@ErrorFamily
public enum PlatformErrorCode {
PERMISSION_DENIED,
DATA_IS_FROZEN,
SERVICE_IS_SUSPENDED,
}

To be able to declare that a REST API can only throw some of the exceptions in this family, define the following annotation:

ThrowsPlatformError.java
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ThrowsPlatformError {
PlatformErrorCode[] value();
}

This annotation satisfies the following 5 conditions:

  • Retention is configured as RUNTIME, i.e. reflectable at runtime

  • Target is configured as METHOD or FUNCTION (kotlin), can be used to decorate methods

  • Has a property named value

  • The value property is of array type

  • The array element type of value is an enum decorated with @ErrorFamily.

tip

If an annotation satisfies all the above conditions, it can be used to decorate REST APIs to declare the exceptions that can be thrown. Just like how we use @ThrowsAll.

UserController.java
@PostMapping("/user")
@ThrowsAll(UserInfoErrorCode.class)
@ThrowsPlatformError({PlatformErrorCode.SERVICE_IS_SUSPENDED})
public void registerUser(@RequestBody RegisterUserInput input) {
...omit code...
}

Export Server Side Exceptions

Throw Exceptions

UserController.java
@PostMapping("/user")
@ThrowsAll(UserInfoErrorCode.class)
@ThrowsPlatformError({PlatformErrorCode.SERVICE_IS_SUSPENDED})
public void registerUser(@RequestBody RegisterUserInput input) {
if (...some condition...) {
throw PlatformException.serviceIsSuspended(
"The service is suspended"
);
}
...omit other code...
}
caution

The kinds of exceptions thrown internally must not exceed those declared externally.

Write Exception Messages to HTTP Responses

tip

As long as Jimmer's Spring Boot Starter is used, no extra work is required. Any exception inherited from CodeBasedException thrown by the service will be automatically translated.

The translation result is:

{
"family":"USER_INFO_ERROR_CODE",
"code":"ILLEGAL_USER_NAME",
"illegalChars": ["&", "`", "@"]
}

To facilitate development and testing, you can configure in application.yml or application.properties:

jimmer:
error-translator:
debug-info-supported: true

This configuration will include information useful for locating issues during development and testing in the HTTP response. Since the content is quite long, please click the button below to view the result.

warning

This configuration is only for assisting development and testing, and must NOT be enabled in production environments!

For example, enable this switch in application-dev.yml and application-test.yml, but absolutely do NOT enable it in application-prod.yml.

Client

Generated TypeScript Code

export type AllErrors =
{
readonly family: "PLATFORM_ERROR_CODE",
readonly code: "SERVICE_IS_SUSPENDED"
} |
{
readonly family: "USER_INFO_ERROR_CODE",
readonly code: "ILLEGAL_USER_NAME",
readonly "illegalChars": ReadonlyArray<number>
} |
{
readonly family: "USER_INFO_ERROR_CODE",
readonly code: "PASSWORDS_NOT_SAME"
} |
{
readonly family: "USER_INFO_ERROR_CODE",
readonly code: "PASSWORD_TOO_SHORT"
} |
...omit other code...
;

export type ApiErrors = {
"userController": {
"registerUser": AllErrors & (
{
readonly family: 'USER_INFO_ERROR_CODE',
readonly code: 'ILLEGAL_USER_NAME',
readonly [key:string]: any
} |
{
readonly family: 'USER_INFO_ERROR_CODE',
readonly code: 'PASSWORD_TOO_SHORT',
readonly [key:string]: any
} |
{
readonly family: 'USER_INFO_ERROR_CODE',
readonly code: 'PASSWORDS_NOT_SAME',
readonly [key:string]: any
} |
{
readonly family: 'PLATFORM_ERROR_CODE',
readonly code: 'SERVICE_IS_SUSPENDED',
readonly [key:string]: any
}
),
...omit other code...
},
"bookController": {
...omit code...
},
...omit other code...
};
  • AllErrors defines all error types

    caution

    Only REST APIs that explicitly declare exceptions that may be thrown via annotations in Java/Kotlin code will be used to generate TypeScript code.

    For example, in the Java/Kotlin code above, we defined the error codes PlatformError.PERMISSION_DENIED and PlatformErrorCode.DATA_IS_FROZEN, but they have not yet been declared as throwable by any REST API using @ThrowsAll or custom @ThrowsPlatformError. So they will not be defined in AllErrors.

  • ❷ The enum name decorated with @ErrorFamily is the family field in client code, used to identify which family the error belongs to

  • ❸ The enum variable name is the code field in client code, used to identify which specific error it is

  • ❹ The server uses @ErrorField (can repeat) to declare arbitrary additional fields for an error code (only one here), so all additional fields are generated here

    tip

    AllErrors defines all errors, never duplicated.

    ApiErrors at ❺ refers to errors, may have duplicates. Because different REST APIs may declare throwing the same error code.

    So additional fields are always declared in AllErrors, not in ApiErrors. This is the fundamental reason AllErrors and ApiErrors are separated.

  • ❺ Errors that each REST API may throw

  • ❻ Errors that userController module may throw

  • ❼ Errors that userController.registerUser API may throw

TypeScript IDE Effects

The above TypeScript code is quite lengthy, and considering some readers may not have TypeScript background knowledge, here are some IDE auto-complete screenshots:

  • Under global type ApiErrors, there are two options: userController and bookController

  • ApiErrors["userController"] has three options: login, logout and registerUser

  • For ApiErrors["userController"]["registerUser"], the family field has two options: USER_INFO_ERROR_CODE and PLATFORM_ERROR_CODE

  • Once family is determined as UserInfoErrorCode, the code field has three options: ILLEGAL_USER_NAME, PASSWORD_TOO_SHORT and PASSWORDS_NOT_SAME

  • Once code is determined as ILLEGAL_USER_NAME, the additional illegalChars field can be used