Serguey Asael Shinder: Files.lines holds the file open until you close the stream
This is correct-looking code that leaks:
long count = Files.lines(path).filter(l -> l.startsWith("ERROR")).count();
Files.lines returns a Stream<String> backed by an open file. The stream implements AutoCloseable, and the javadoc says in terms that it should be used in a try-with-resources block. Nothing in the type system enforces that, because Stream is AutoCloseable but is almost never treated as a resource.
The fix is one construct:
try (Stream<String> lines = Files.lines(path)) {
return lines.filter(l -> l.startsWith("ERROR")).count();
}
Why it usually looks fine
A leaked descriptor is invisible until you run out, and the garbage collector will eventually close the handle when the stream becomes unreachable — so a test that reads three files passes, and a service that reads one file per request dies on a Tuesday with java.nio.file.FileSystemException: Too many open files. The stack trace at that point points at whichever innocent call happened to need the next descriptor, not at the leak.
On Windows the failure arrives sooner and looks different: the open handle keeps a lock, so the "leak" shows up as an inability to delete or rename the file, often in a cleanup step far from the read.

The same trap, other methods
The rule is: any method that returns a Stream reading from the file system returns a resource.
Files.lines(path)Files.list(dir)Files.walk(dir)Files.find(dir, depth, matcher)Files.newDirectoryStream(dir)— this one at least looks like a resource
The counterexample worth knowing is Files.readAllLines, which reads everything into a List and closes the file before returning. It has no resource semantics because it has no laziness. That is the real trade: lines exists so you can process a file larger than memory, and the price of the laziness is that the file stays open while you are lazy.
Two details that bite
A returned stream is a returned resource. A method like Stream<String> readErrors(Path p) hands the caller an obligation, and callers will not honour it unless the signature and the javadoc say so loudly. If you can, return a List, or take a consumer and own the closing yourself:
public long countErrors(Path p) throws IOException {
try (Stream<String> lines = Files.lines(p)) {
return lines.filter(this::isError).count();
}
}
IOException during iteration becomes UncheckedIOException. The read happens lazily, inside the terminal operation, long after Files.lines returned — so an I/O error mid-file cannot be a checked exception and arrives wrapped. A catch (IOException e) around the whole block will not catch it.
Related: the exception that closed your resource hides the real one.