Introduction
In my Java application, I need to represent the result of an operation. This can either be OK or ERROR + an error message.
Background
I have a class to verify the integrity of a directory (e.g. to check that a few conditions hold). If it finds out the directory is corrupted, it should return a description of the error, such as:
- Unexpected file - [filename].
- Unexpected number of files - expected [X], actual [Y].
The callers of the class don't know anything about the directory and the conditions. They are only interested to know if the directory is OK or not, and in that case, to get an appropriate error message.
The Result class was intended as a way to transmit the results of the verification.
I chose not to throw exceptions as (to me) that seemed more convenient for reporting errors in the verification process. It seemed inappropriate to intermix those two situations (e.g. the directory was found invalid vs. an error occurred while verifying the directory).
The current solution
I have so far been able to come up with the following class.
public class Result
{
private static enum State { OK, ERROR };
private State state;
private String errorMessage;
private Result(State state, String errorMessage)
{
this.state = state;
this.errorMessage = errorMessage;
}
public static Result ok()
{
return new Result(State.OK, null);
}
public static Result error(String errorMessage)
{
return new Result(State.ERROR, errorMessage);
}
public boolean isOK()
{
return (this.state == State.OK);
}
public String getErrorMessage()
{
return errorMessage;
}
}
The client code then looks like this:
Result result = Result.ok();
Result result = Result.error("Could not ...");
if (result.isOK())
...
else
{
String errorMessage = result.getErrorMessage();
...
}
This has the benefit that when creating a result, the client only has to deal with error messages in case of an error.
On the other hand, the API does not prevent the client code to ask for an error message in case of an OK result. That does not feel right.
I also don't like the way the information about valid states (OK/ERROR) gets duplicated into:
- the internal enumeration,
- the public static methods.
What do you think about my implementation? How would you design the class?
5 Answers 5
After Java 17, you can perhaps even use sealed classes for this: https://openjdk.org/jeps/409
Something like:
sealed interface IOResult permits Ok, Err {
record Ok() implements IOResult {}
record Err(IOException error) implements IOResult {}
}
class FileManager {
IOResult writeFile(Path path, String contents) {
try {
IOUtils.writeFile(path, contents);
return new Ok();
} catch (IOException e) {
return new Err(e);
}
}
}
Then you can do Rust-y stuff like:
IOResult res = fileManager.writeFile(path, contents);
return switch (res) {
case Ok -> ???;
case Err(var error) -> ???;
}
-
\$\begingroup\$ Your code looks very much like Rust! doc.rust-lang.org/std/result/enum.Result.html \$\endgroup\$Nayuki– Nayuki2025年02月23日 05:36:02 +00:00Commented Feb 23 at 5:36
I do not know the exact requirements, but two simple approaches would be:
Just return a String as result. This can either be null or empty (if everything is ok) or the error message (if something is wrong).
Throw an exception
(Other then that: I would try to find a better name for the factory methods. The meaning of the current names is not really clear.)
edit after original post was changed:
If you do not need a general solution or anything special, I would keep the simple way and return a String. KISS principle.
If you want to make it fancy, I would continue with your approach, only slightly modified:
public class DirectoryChecker
{
private String errorMessage;
//or private List<String/anything else> errors;
public static Result ok()
{
return new Result(State.OK, null);
}
public boolean isThereAnyProblem()
{
// check if everything is ok. Set error if not.
// if the check should be done only once, add a member variable to keep the state
}
public String /* or anything else */ getErrorMessage/*s*/()
{
return errorMessage /*or anything else*/;
}
}
Explanations:
- Some factory method will create an instance of this class. The caller can do:
if(directoryChecker.isThereAnyProblem()) { get and handle error }
- I do not think you need the enum. It represents a state of the internal state. And you only need ok/not ok as far as I got it.
- As you see this is hardly the same as the String approach, you just wrap an additional class around it.
Side note: You could name the check method isEverythingOk, but personally I prefer this flow of code:
if (something happened)
{ handle it }
go on with normal flow
instead of
if (everything is ok)
{ do normal things }
else
{ handle it }
go on with normal(?) code or other code(?)
About exceptions: I would not use them in this case, it makes life only more complex.
-
\$\begingroup\$ I updated the question and described why I did not choose exceptions to transmit the error message. \$\endgroup\$Dušan Rychnovský– Dušan Rychnovský2013年01月09日 09:19:17 +00:00Commented Jan 9, 2013 at 9:19
-
\$\begingroup\$ @DušanRychnovský I have updated my post to reflect your changes \$\endgroup\$tb-– tb-2013年01月09日 11:55:01 +00:00Commented Jan 9, 2013 at 11:55
-
\$\begingroup\$ @tb- I don't think that simply returning a string would be a great idea. I feel better when the domain concepts have their own class and I don't have to guess what is the meaning of a string \$\endgroup\$mariosangiorgio– mariosangiorgio2013年01月09日 13:58:42 +00:00Commented Jan 9, 2013 at 13:58
I'd change your enum State, like this:
enum State
{
OK(null),
COULDNT_LOAD("Error: Could not load file: %s."),
COULDNT_PARSE("Error: Could not parse contents of file %s."),
UNEXPECTED_NUM_FILES("Error: Unexpected number of files - expected %d, actual %d.");
String message;
State(String message)
{
this.message = message;
}
void setReplacements(Object... replacements)
{
if(this.message != null)
{
this.message = new Formatter().format(this.message, replacements).toString();
}
}
}
Then you can have your validation method return a state, and you can do your checking block after, like this:
File fileITriedToLoad = new File("/usr/file.java");
State result = State.COULDNT_LOAD;
result.setReplacements(fileITriedToLoad.getAbsolutePath());
if(result != State.OK)
{
System.out.println(result.message);
}
And you can delete the rest of your Result
class.
Edit: Using the new setReplacements()
method would then allow you to add pieces of text at the specified points in your message, so your validation method that returns a State would use it as follows:
State state = State.UNEXPECTED_NUM_FILES;
state.setReplacements(1, 3);
And then printing the state.message
will produce:
Error: Unexpected number of files - expected 1, actual 3.
-
\$\begingroup\$ It seems that in this case, the error message would always be like "Okay" or "Error". That's not what I need. I need it to be either undefined (if the state is OK), or a custom error message, like "Could not open file XXX." (if the state is ERROR). Moreover, multiple error messages should be expressible (e.g. "Could not open file XXX.", "Could not parse contents of file XXX.", etc.). \$\endgroup\$Dušan Rychnovský– Dušan Rychnovský2013年01月08日 20:52:14 +00:00Commented Jan 8, 2013 at 20:52
-
\$\begingroup\$ @DušanRychnovský Well then, if you know what file they tried to load, see my edits. \$\endgroup\$MrLore– MrLore2013年01月08日 20:58:32 +00:00Commented Jan 8, 2013 at 20:58
-
\$\begingroup\$ @DušanRychnovský And to pre-empt the next question: if you don't know the file, wrap the file and Status into a
ValidatedFile
class with agetState()
andgetFile()
method. \$\endgroup\$MrLore– MrLore2013年01月08日 21:06:13 +00:00Commented Jan 8, 2013 at 21:06 -
\$\begingroup\$ I need a more general solution. I updated the question to make this more apparent. I admit I have not made it clear in my previous comment that not all error messages are related to a single file. \$\endgroup\$Dušan Rychnovský– Dušan Rychnovský2013年01月09日 09:17:49 +00:00Commented Jan 9, 2013 at 9:17
-
\$\begingroup\$ @DušanRychnovský Edited accordingly :) \$\endgroup\$MrLore– MrLore2013年01月09日 13:04:46 +00:00Commented Jan 9, 2013 at 13:04
If you don't want to use exceptions you should know that in OOP there is a lot of ways to implement the fact that some object belongs to some specific class. It can be either some enum that enumerates all possible domains or in case of OOP languages it can be class.
So in case of Java (again if you don't want exceptions by some reason) you can use:
class Result
{
public static final Result ok = new Result(); // I'm not sure this is correct in terms of Java
}
class Failure extends Result
{
public final String errorMessage;
public Faliure(String message)
{ this.errorMessage = message; }
}
if (result instanceof Failure)
{
// do something
}
-
1\$\begingroup\$ I like this idea. The only (subtle) drawback I see is that you have to explicitly cast the result to a Failure in order to obtain the error message. You also have to know the implementation details (e.g. that there is a class hierarchy) to be able to distinguish between an OK and an ERROR state. \$\endgroup\$Dušan Rychnovský– Dušan Rychnovský2013年01月09日 09:22:45 +00:00Commented Jan 9, 2013 at 9:22
-
\$\begingroup\$ @DušanRychnovský, you need to know that as well in case of
enum
and others. You can tear off need of hierarchy by using interfaces. You can add continuation behavior and makeResult
to provideResult RunWithSuccess(/* some action representation */)
which will do nothing but returnthis
forFailure
and result of action execution forok
. \$\endgroup\$ony– ony2013年01月09日 10:01:48 +00:00Commented Jan 9, 2013 at 10:01 -
\$\begingroup\$ @DušanRychnovský if you don't have a hierarchy like that you are going to have an ok result coming with an error message and that does not make too much sense to me \$\endgroup\$mariosangiorgio– mariosangiorgio2013年01月09日 13:57:06 +00:00Commented Jan 9, 2013 at 13:57
-
\$\begingroup\$ @mariosangiorgio exactly - that's one of the drawbacks I see with my own solution and one of the reasons why I like this one. \$\endgroup\$Dušan Rychnovský– Dušan Rychnovský2013年01月09日 14:33:29 +00:00Commented Jan 9, 2013 at 14:33
-
\$\begingroup\$ The problem with this is that it is implied that
OK
is aResult
, and so aResult
isOK
. It would be better to have a another class to representOK
. \$\endgroup\$Bobby– Bobby2025年02月21日 10:47:48 +00:00Commented Feb 21 at 10:47
I am convinced that for some use-cases more than one error may happen, that are more or less separate cases. And some can be dealt with differenty. All on a case basis.
And no error messages means okay!
public class Result
{
private Set<String> errorMessages;
public boolean isOK()
{
return errorMessages.isEmpty();
}
public Set<String> getErrorMessages()
{
return errorMessages;
}
Error codes might be considered too, but at least this diversification allows errors to flow in from different sources while retaining information on all error cases.
The errors for "unexpected file" and "unexpected number of files" are likely to stem from different locations. In the case of exceptions thrown and caught, some collection or arbitration of errors is needed anyway.
Notice the single Result type for the all-okay state has a disarming getErrorMessages()
- to know: the empty set.
Optional
does also not prevent a client from querying the value if non is present, instead it throws an error as it is basically a "misuse". \$\endgroup\$