×
Community Blog Tips for Designing Error Codes in Java

Tips for Designing Error Codes in Java

What makes an error message "good"? In this blog, we share some tips on designing effective error codes in Java.

By Leijuan

What Is a Good Error Message?

Error messages are often taken for granted. Of course, most of us wish to never encounter one especially when writing codes. But when it is absolutely necessary, error codes should be clear and instructive enough to help you troubleshoot the issue you are facing.

A good error message consists of three parts:

The Error Itself: What exactly is the error about? Are the specific reasons and data logs at that time?
Context: What caused the error? What is the code expected to do when an error occurs?
Mitigation: What can you do to rectify this error?

The three points above may seem obvious at first glance, but there several nuances that we need to pay attention to in order to make these points less abstract.

Let's take a look at a real code example, which is a jdoctor project from an author at Oracle Labs [1]. The sample code is as follows:

 ProblemBuilder.newBuilder(TestProblemId.ERROR1, StandardSeverity.ERROR, "Hawaiian pizza")
                .withLongDescription("Pineapple on pizza would put your relationship with folks you respect at risk.")
                .withShortDescription("pineapple on pizza isn't allowed")
                .because("the Italian cuisine should be respected")
                .documentedAt("https://www.bbc.co.uk/bitesize/articles/z2vftrd")
                .addSolution(s -> s.withShortDescription("eat pineapple for desert"))
                .addSolution(s -> s.withShortDescription("stop adding pineapple to pizza"));

The core of the code includes the following fields:

Context: such as app name, component, status code. You can use a string to describe the context at that time, such as application name, component name, specific error status code. It is up to you to decide to use which one. Of course, JSON string can also be used, such as {"app":"uic", "component": "login", "code":"111"}.

Description: Long and Short to describe error. To describe errors, you can choose Long or Short description.

Because: explains the reason with data. It explains causes of the error in detail. Of course, the corresponding data must be included.

documentedAt: provides an error link. It provides the HTTP connection corresponding to the error, and the error is described in detail.

Solutions: describes possible solutions. For example, the solutions prompt visitors to check whether the spelling of an email is correct or whether the Pass Code of a text message is entered correctly.

With these specific fields, it is much easier for us to understand the code.

Design of Error Codes

Error codes are essential for error handling. They have many benefits, including uniqueness, ease of search, and usage for statistics. There are many design specifications for error codes on the Internet, so I wouldn't delve into the details in this article. The design and methodology discussed in this article is provided only for your reference. You can judge its suitability based on your actual needs.

An error code usually consists of three parts:

System/App short name: the name of a system or application, such as RST and OSS. If you are familiar with Jira, you are familiar these specifications. Java programmers are supposed to know what HHH and SPR represent, aren't they?

Component short name or code: the name or code of components within the system, such as LOGIN, AUDIT, and 001, which can be used to locate errors more quickly.

Status code: error status codes. It is a three-digit status code, such as 200, 404, and 500, which are designed based on HTTP status codes. After all, most developers know HTTP status codes, so we do not need to redesign error status codes.

With the above specifications, let's look at a typical error code:

OSS-001-404: It indicates that a certain component of OSS reports that the resource was not found.

RST-002-500: This is an internal error of a component.

UIC-LOGIN-404: It indicates that the specified account cannot be found when a member performs logon.

The common practice is to use an application abbreviation, component name or code, as well as status value, and then connect them with hyphens. Generally speaking, the hyphen is easier to read, while an underscore sometimes is regarded as a space when displayed. At the same time, with the support of standard HTTP status codes, you can guestimate the meaning of error codes without referring to documents.

The design of error codes should not be too complicated. If you add all the information into an error code, the error code would be complete but it would also increase the cost of developers to understand and use those error codes. At the end of the day, as a developer, you'll need to make a judgment call and make the right compromise based on your understanding and experience. It may also be useful for you to perform usability testing on your error codes, or even research on the preferences of your users such as in human-computer interaction (HCI) design.

In the error mentioned above, it is actually used to activate contexts, such as UIC-LOGIN-404. Where did the error occur? The error code helps you locate it. What was the code expected to do at that time? The error code also explains this. Although an error code cannot completely represent the context of the error, the information carried by the error code is enough to help us understand the context at that time. Therefore, the error code here plays the role of contexts. At present, it seems that at least the error code is more convincing and standardized than the "Hawaiian pizza" example of the ProblemBuilder.newBuilder(TestProblemId.ERROR1, StandardSeverity.ERROR, "Hawaiian pizza") statement we've seen earlier.

Coding Format of an Error Message

After the error code design is completed, we still cannot simply output an error in the format of “an error code + a short message”. Otherwise, we may encounter situations like ORA-00942: table or view does not exist. Faced with this message, you may think: “Why don't you specify which table or view it is?" Therefore, we also need to design a message format that includes context, description, reason, document link, and solutions about an error, which will be more friendly to developers. Here I have drawn up a Message specification in the following part.

long description(short desc): because/reason --- document link – solutions

Let me explain the logic of this message:

The long description of an error is written directly, and the short description is included in parenthesis. This kind of writing is very common in contracts, such as Alibaba Cloud Computing Co., Ltd. (Alibaba Cloud). When you sign a labor contract, the company name is generally written in the format as “full name (abbreviated name)”. Many developers will write "Login Failed" in the error log, but the login system supports many login methods. Therefore, it is far less clearer than Failed to log in with email and password(Login Failed) , Failed to log in with phone and passcode(Login Failed) and Failed to log in with oauth2(Login Failed) .

The specific reason for the error: The next part is a colon, and after the colon is a detailed reason. For example, “email user@example.com not found, gender field is not allowed in package.json” must contain specific data information, including input information. It is as same as a labor contract in which your name and company name are followed by your specific position and salary. Although the contract is formatted, but specific position and salary are different between employees and those parameters are obtained from the outside. In face with this situation, a colleague from the security department asked how to desensitize data. This is another problem. Most developers are supposed to know how to perform data masking. Let's skip it here. When the error of labor dispute occurs, the Labor Arbitration Bureau can quickly locate and solve the “error” because of the data of the specific causes, such as position and salary.

Document link: Next, three hyphens (---) are used to separate the rest parts and enter the corresponding error link. Three hyphens are used as delimiters in many scenarios, such as mdx and yaml, which are familiar to developers. If there is no link, just ignore it.

Solutions: Normal text expression is enough, which can be explained clearly, and it is also placed after the three hyphens.

Take a look at a specific example of the message format:

APP-100-400=Failed to log in system with email and password(Email login failed): can not find account with email {} --- please refer https://example.com/login/byemail  --- Solutions: 1. check your email  2. check your password

The description corresponding to the error code, APP-100-400, above basically covers the information required by jdoctor. The description of this error is very complete and is convenient for subsequent log analysis.

Assembling and Storing Error Codes and Messages

With specifications of error codes and messages, how do we store the information next? If it is in Java, is it required to create the corresponding ErrorEnum and Plain Ordinary Java Objects (POJO)? Here, I recommend that you use the properties file to store the information about error codes and messages. The file can be directly named as “ErrorMessages.properties”, which is in a package. The file sample is as follows:

### error messages for your App
APP-100-400=Failed to log in system with email and password(Email login failed): can not find account with email {0} --- please refer https://example.com/login/byemail  --- Solutions: 1. check your email  2. check your password
APP-100-401=Failed to log in system with phone and pass(Phone login failed): can not find account with phone {0} --- please refer https://example.com/login/byphone  --- Solutions: 1. check your phone  2. check your pass code in SMS

Why do you need to select the properties file to store the information about error codes and messages? It is mainly for the following reasons:

International support: Developers using Java all know that if you want to adjust error messages to Chinese, you can just create a ErrorMessages-zh_CN.properties. The suggestion in the original text is Don't localize error messages, but considering that most domestic programmers may not be able to express clearly in English, Chinese is also acceptable.

• Various languages support file parsing of properties, not only Java, but also other languages. In addition, the properties file itself is not complicated, so the properties file can be used in other languages such as Node.js and Rust. However, it is mostly impossible for Java enum and POJO to use other languages.

• Properties file formats are rich, supporting comments, line feeds, multi-line escape, etc.

1

Finally, the most important thing is that they are fully supported in IDE. For IntelliJ IDEA used by Java developers, the support for properties files seems to be extreme, as follows:

• Automatic prompts for error codes

2

• Quick check: You can just move the cursor up, press CMD and move the cursor up, or press Alt and Space, not to mention clicking directly to locate the error code.

3

• Refactoring and lookup support: Although an error code is a string, it is also the key to properties. Therefore, if you rename an error code, all referenced parts will be renamed. It also supports find usage to check where the error code is referenced. It is very convenient. Of course, if an error code is not used in the system, it will also be marked in grey.

• Automatic display of folded contents: When your codes are in a folded state, IDEA directly brings messages to display. It is much more convenient for you to review codes and understand them.

4

• Directly modify the value of messages

5

In short, IntellIJ IDEA supports properties files to the extreme. We are supposed to consider the developer experience. It is harmful for the developer experience to make developers jump around to find error codes everywhere, so we cannot do this kind of things. Of course, other IDE and WebStorm by JetBrains also support to edit properties files.

Code Implementation

The features above seem fantastic. How can we use it? Does it require the import of a development package? No, you only need 10 lines of code, as follows:

import org.slf4j.helpers.MessageFormatter;

public class AppErrorMessages {
    private static final String BUNDLE_FQN = "app.ErrorMessages";
    private static final ResourceBundle RESOURCE_BUNDLE = ResourceBundle.getBundle(BUNDLE_FQN, new Locale("en", "US"));
    public static String message(@PropertyKey(resourceBundle = BUNDLE_FQN) String key, Object... params) {
         if (RESOURCE_BUNDLE.containsKey(key)) {
            String value = RESOURCE_BUNDLE.getString(key);
            final FormattingTuple tuple = MessageFormatter.arrayFormat(value, params);
            return key + " - " + tuple.getMessage();
        } else {
            return MessageFormatter.arrayFormat(key, params).getMessage();
        }
    }
}

Therefore, if you want to print error messages anywhere, you can just write log.info(AppErrorMessages.message("APP-100-400","xxx"));. If you want to conduct wrapper with logs, such as log.info("APP-100-400","xxx"). There is no problem with that, and the sample code is as follows:

public class ErrorCodeLogger implements Logger {
    private Logger delegate;
    private static final String BUNDLE_FQN = "app.ErrorMessages";
    private static final ResourceBundle RESOURCE_BUNDLE = ResourceBundle.getBundle(BUNDLE_FQN, new Locale("en", "US"));

    public ErrorCodeLogger(Logger delegate) {
        this.delegate = delegate;
    }

    @Override
    public void trace(@PropertyKey(resourceBundle = BUNDLE_FQN) String msg) {
        delegate.trace(RESOURCE_BUNDLE.getString(msg));
    }
}

Next, you can directly integrate error codes in logs. It is very convenient. I have already written the above code, and you can refer to the project address at the end of the article.

6

The final log output is as follows:

7

Note: Here we use the MessageFormatter of SLF4J, mainly to facilitate the subsequent integration of SLF4J. The MessageFormatter of SLF4J is better than that of Java in fault tolerance and performance.

FAQ

Why Do We Choose 3-digit HTTP Status Codes as Error Status Codes?

Most developers are familiar with HTTP status codes, so they roughly understand what those error status codes mean. However, there are also strict requirements for application developers. The “404” cannot be interpreted as an internal error, such as database connection failure, and things that go against normal thinking should be avoided. HTTP status codes are classified as follows, and you can also refer to the HTTP Status Codes Cheat Sheet [2].

• Informational responses (100–199)
• Successful responses (200–299)
• Redirection messages (300–399)
• Client error responses (400–499)
• Server error responses (500–599)

However, error status codes are not limited to HTTP status codes. You can refer to SMTP, POP3 and other status codes. In addition, you can also choose codes such as 007 and 777, as long as you can explain them reasonably.

In our daily life, we will use some numbers with special meanings or homophonic sounds. However, there are some precautions, especially when dealing with an international audience of developers. Here are some examples:

• UIC-LOGIN-666: To describe smooth, perfect login. However, "666" is a local Internet slang in China to describe something good, but it means something completely different overseas. If there are foreigners from Europe and the United States in your team, they may understand it as malicious login or login failure.

• APP-LOGIN-062: If your team has Hangzhou natives, do not use the number 62.

• APP-001-013: If the error code is to be transmitted to the end user, avoid using the number 13 as it may be considered unlucky in some cultures.

Numbers with special meanings or homophonic sounds in Chinese, such as 520, 886, 999, and 95, are convenient to understand and friendlier if they are used properly. For example, it would be friendlier to transmit UIC-REG-520 to a user instead of UIC-REG-200 (Registered Successfully) in China. Generally speaking, we should pay attention to the actual usage scenarios and our target audience when using these numbers. Of course, to be safe, you can always refer to status codes designed by HTTP, SMTP, and so on.

Is Properties File Really Better than Enum and POJO to Store Error Codes and Messages?

In terms of the support of Java and IntelliJ IDEA, the current cooperation is relatively good, such as internationalization and maintenance costs. These ErrorMessages.properties can also be submitted to the central warehouse for centralized management of error codes. However, Java enum and POJO are troublesome for both internationalization and centralized management, and the number of code is relatively huge, which can be seen from the preceding problem builder of jdoctor. However, it may not be absolute in different languages. For example, in Rust, because enum has rich features, it may be a better choice to use enum to implement error codes in Rust.

#[derive(Debug)]
enum ErrorMessages {
    AppLogin404 {
        email: String,
    },
    AppLogin405(String),
}

impl fmt::Display for ErrorMessages {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        // extract enum name parameter
        // output message from java-properties
        write!(f, "{:?}", self)
    }
}

Why Are Error Levels Not Provided in Error Codes?

An error level is added in the design of many error codes, such as RS-001-404-9, whose last digit indicates the severity of the error. There is no problem in doing so, but practical factors should also be considered.

• Error levels will be dynamically adjusted: For example, with the change of time and place, the error level that was very serious before is not so serious now. If the resource cannot be found, it might be very serious before, but now it can be searched again from the backup server with an added backup scheme. Therefore, this error may not be so serious currently on the main service.

• Perceptions of an error level vary among different teams: For example, the OSS-404 error cannot be found on the data server of the OSS team. The metadata is all available, but no corresponding data is found on the data server. It is a very serious error. I am in the business team. If I am in charge of Serverless Jamstack, one of the files missing, such as an HTML, CSS, or image, may not be a big problem. I can wait for a while to try again, and upload over again if it failed. What I want to say is that the same error does not matter similarly among different teams.

If errors are basically fixed into error codes, you cannot adjust them later. If you adjust the error level, it may become another error code, which will cause problems for statistics and understanding. In my opinion, an error code should not include the information of error severity, but be explained through peripheral documents and descriptions. Of course, you can also determine the error level by log.info and log.error.

Can a Shared Library Be Offered?

Since IntelliJ IDEA does not support dynamic properties file names, you cannot perform code prompt, search and other features if you use dynamic properties file names Thus, it must be static properties file names, such as @PropertyKey(resourceBundle = BUNDLE_FQN). As for a Java class, you can copy just this Java class. After all, you can do it once and for all. In addition, it is more convenient for you to personalize and adjust codes. For example, it is easier to integrate with Log4j 2.x or the customized logging framework. Logs are the most basic requirements of a project. Therefore, when you create a project, you are supposed to add the code corresponding to error codes to the project template, so that the features of logging and error codes will be automatically included after the project is created.

Other Considerations

The original text and related discussions on Reddit also carried out some arrangement and explanation:

• Note the differences between internal and external environments: For example, errors of internal developers may include the specific information of the server. However, the end consumers, such as FaaS developers of the platform, shall not receive such information which may entail a certain security risk.

• Be careful not to leak sensitive data in errors: The data output to error logs must be masked, but it should not affect error locating. It depends on specific scenarios.

• Do not use error messages as API contracts: In API scenarios, there are two ways to respond to errors: One is to respond according to error codes, such as REST API. The other is to respond according to messages, such as GraphQL. Thus, you can choose it.

• Note the consistency of error codes: Error messages will be output to different consumers, such as REST API, interface, so prompt messages of errors may be different, including internationalization, desensitization and so on. However, it is better to use the same error code, that is, front end and back end share the same error code, which is easy to locate errors and keep statistics.

Summary

This design that uses error codes and properties files to store error messages is a comprehensive choice. If IDEA cannot support properties file well, you cannot directly locate an error message when you see an error code. On the contrary, you need to find the corresponding message everywhere. Therefore, enum and POJO may be good choices. In addition, the design of error codes is similar to the scheme of HTTP status codes. This is mainly based on the fact that everyone is very familiar with HTTP and can roughly guess the general meaning of those error codes. However, randomly coded numbers do not have the advantages of this method, and developers need to search it in the error code center, which is a waste of developers' time.

Reference

[1] https://github.com/melix/jdoctor
[2] https://cheatography.com/kstep/cheat-sheets/http-status-codes/
[3] https://www.morling.dev/blog/whats-in-a-good-error-message/

0 0 0
Share on

Alibaba Cloud Community

863 posts | 196 followers

You may also like

Comments