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.

SyntaxReturnType 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 need name = name.trim();. The same goes for BigDecimal.add and the java.time methods such as plusDays.
  • 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 -1 for errors. When a failure is exceptional, throw an exception; when absence is normal, use Optional.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

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.

Example · java
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.

Example · java
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: false

Guard 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.

Example · java
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

  • Be the first to comment on this lesson.