When does EK9 close a closeable value, and where may I keep one?
← Error Handling and Exceptions · Ref: Q1520
A closeable value is one whose type has an 'operator close' (a TextFile's input() or output(), a TCP or UDP connection, a Server, a Transaction, your own class). EK9 decides who closes it from WHERE it lives, so a leak cannot be written:
A closeable value you never NAME is owned by the evaluation that created it - usually its statement - and is closed as soon as that evaluation finishes - normally, when a stream stage such as 'head' stops early, or when an exception leaves it (an operand that runs only sometimes, such as the right of 'and', closes its own: see Q1940):
lines <- List() of String cat TextFile("data.txt").input() > lines
The input is closed as soon as that cat has finished, so this can run thousands of times without running out of file handles. Two unnamed closeables in one statement are closed last-made first.
A closeable value you NAME must be declared as a try-with-resources resource. It stays open for the whole try (and its catch and finally) and is closed when the try ends:
try -> input <- TextFile("data.txt").input() while input.hasNext() stdout.println(input.next())
Holding one in a plain local variable is a compile error, because nothing would ever close it:
input <- TextFile("data.txt").input() // ERROR E81056 stdout.println(input.next())
Two places may hold a closeable without a try. RETURNING one hands it to the caller, who then names it in a try (or leaves it unnamed):
openLog()
<- rtn as CloseableStringInput: TextFile("log.txt").input()
and a PROPERTY may hold one - the object owns it, and its type's close ('default operator close', or the generated part of an 'operator close' you write) closes it, then calls the super class's close. Closing is generated, never written: there is no 'close' statement (E01094) and the close operator is never called as a method (E81060), so only the owner's generated code ever closes a value (see Q1600). A standard stream (Stdin, Stdout, Stderr) is borrowed, not owned, and is never closed; so are the input and output of a TCP connection, which belong to the connection.
The program below shows each case with a closeable whose close prints.
Example
defines module qa.errorhandling.closeableownership defines class Tracker name as String? default private Tracker() Tracker() -> label as String name: label describe() as pure <- rtn as String: `tracker ${name}` operator close as pure stdout <- Stdout() stdout.println(`closed ${name}`) default operator ? Owner tracker as Tracker? default private Owner() Owner() -> label as String tracker: Tracker(label) describe() as pure <- rtn as String: tracker.describe() default operator close default operator ? defines function makeTracker() -> label as String <- rtn as Tracker: Tracker(label) defines program CloseableOwnershipDemo() stdout <- Stdout() stdout.println(Tracker("unnamed").describe()) try -> named <- Tracker("named") stdout.println(named.describe()) stdout.println("still open inside the try") try -> made <- makeTracker("made") stdout.println(made.describe()) try -> owner <- Owner("owned") stdout.println(owner.describe())
Common mistakes
E81056 — A closeable value held in a named local variable must be declared as a try-with-resources resource, or nothing would ever close it. See ek9 -h E81056 for details.
Incorrect:
named <- Tracker("named") stdout.println(named.describe()) stdout.println("still open inside the try")
Correct:
try -> named <- Tracker("named") stdout.println(named.describe()) stdout.println("still open inside the try")
Other ways to ask this
- Is 'cat TextFile(path).input() > lines' closed after it reads the file?
- Why must a named closeable be declared in a try?
- Who owns a closeable value I never assign to a variable?
- How do I keep a file or a connection open across several statements?
- Can I return a closeable from a function or keep it in a property?
Coming from another language?
Java: try-with-resources is optional - an AutoCloseable in a plain local, or created inline as new FileReader(path).read(), compiles and leaks; only SpotBugs or an IDE inspection notices. Python: 'with open(...)' is optional too; open(path).read() relies on reference counting to close. C#: 'using' is optional; CA2000 is a warning. Rust: Drop closes everything at scope end. EK9: an unnamed closeable is closed by its statement, a named one must be a try-with-resources resource (E81056) - there is no third place for a leak to live.
Keywords: leak, try-with-resources, operator close, try, resource, ownership, cat, E81056, input, file handle, closeable, close, TextFile