Return Values
Return results cleanly: early returns for guard checks, records for several values, Optional for maybe-missing results and empty lists instead of null.
ReturnType name(params) {
if (badInput) return fallback; // early return
return result;
}
void name(params) { if (done) return; }A method declares the type of value it hands back, and return ends the method and passes that value to the caller. A void method returns nothing; it can still use a bare return; to stop early.
Early returns keep handlers flat
Checks that reject bad input are clearer at the top of a method, each returning straight away, than nested inside one another. This is often called a guard clause. The happy path then runs down the left margin with no extra indentation:
int status(String token, String role) {
if (token == null) return 401;
if (!role.equals("admin")) return 403;
return 200;
}
Returning more than one value
A Java method returns exactly one value. When you need two or three, return a record. A record such as PageInfo(int offset, int totalPages) names each part, which an int[] or Object[] never does, and it costs one line to declare. Avoid using a Map<String, Object> as a bag of results: the compiler cannot check the keys or the types.
When there may be no result
Returning null to mean "not found" pushes a hidden rule onto every caller, and the one that forgets turns into a NullPointerException in production. Two better options:
- A single item that may be missing: return
Optional<T>. The type itself tells the caller to handle the empty case, for example by mapping it to a 404. - A collection: return an empty list, set or map, never
null. Callers can loop, stream and serialise an empty list with no special case, and a JSON API sends[], which clients expect.
Optional is designed as a return type. It is not meant for fields or parameters, and a method declared to return Optional must never return null itself.
Mistakes people make
- Ignoring a return value.
name.trim();on its own line does nothing useful because strings are immutable; you needname = name.trim();. The same goes forBigDecimal.addand thejava.timemethods such asplusDays. - Returning an internal mutable collection directly, which lets callers change the object's state. Return an unmodifiable view or a copy.
- Using a return code such as
-1for errors. When a failure is exceptional, throw an exception; when absence is normal, useOptional.
Example
When to use it
- Return Optional<User> from a repository's findById so the controller maps the empty case to a 404.
- Return a record with both the page of results and the total count for a paginated list endpoint.
- Return an empty list for a user with no orders so the API sends [] instead of null.
More examples
Optional for a lookup that may miss
The Optional return type forces the caller to decide what an absent user means, here a 404. Map.get returns null for a missing key, so Optional.ofNullable converts it at the boundary.
final Map<Integer, String> users = Map.of(1, "ada", 2, "linus");
Optional<String> findUser(int id) {
return Optional.ofNullable(users.get(id));
}
String getUser(int id) {
return findUser(id)
.map(name -> "200 {\"name\":\"" + name + "\"}")
.orElse("404 {\"error\":\"not_found\"}");
}
void main() {
IO.println(getUser(1));
IO.println(getUser(9));
}
// Output:
// 200 {"name":"ada"}
// 404 {"error":"not_found"}An empty list, never null
Because rolesFor never returns null, the caller can call size() and contains() without a null check. Serialised to JSON, the empty result becomes [] rather than null.
final Map<String, List<String>> rolesByUser = Map.of(
"ada", List.of("admin", "editor"));
List<String> rolesFor(String user) {
return rolesByUser.getOrDefault(user, List.of());
}
void main() {
for (String user : List.of("ada", "guest")) {
List<String> roles = rolesFor(user);
IO.println(user + " has " + roles.size() + " role(s): " + roles);
IO.println(" can edit: " + roles.contains("editor"));
}
}
// Output:
// ada has 2 role(s): [admin, editor]
// can edit: true
// guest has 0 role(s): []
// can edit: falseGuard clauses with early returns
Each check exits as soon as it fails, so the order of the checks matches the order of the HTTP status rules: authentication, then permission, then input. The final return is reached only when everything passed.
record Request(String token, String role, String body) {}
int authorise(Request req) {
if (req.token() == null || req.token().isBlank()) {
return 401;
}
if (!req.role().equals("admin")) {
return 403;
}
if (req.body().isBlank()) {
return 400;
}
return 200;
}
void main() {
IO.println(authorise(new Request("", "admin", "{}")));
IO.println(authorise(new Request("t-123", "viewer", "{}")));
IO.println(authorise(new Request("t-123", "admin", " ")));
IO.println(authorise(new Request("t-123", "admin", "{}")));
}
// Output:
// 401
// 403
// 400
// 200
Discussion