apple/pkl · error · IllegalStateException
External reader process has already been closed.
Error message
External reader process has already been closed.
What it means
ExternalReaderProcessImpl.getTransport hands out the message transport for an external reader subprocess, but only while the reader is still open. If close() was already called, calling getTransport (via getModuleResolver/getResourceResolver/spec accessors) throws this IllegalStateException, signalling a lifecycle violation: use-after-close of the reader process.
Source
Thrown at pkl-core/src/main/java/org/pkl/core/externalreader/ExternalReaderProcessImpl.java:89
: null;
}
@Override
public ExternalModuleResolver getModuleResolver(long evaluatorId)
throws ExternalReaderProcessException {
return ExternalModuleResolver.of(getTransport(), evaluatorId);
}
@Override
public ExternalResourceResolver getResourceResolver(long evaluatorId)
throws ExternalReaderProcessException {
return ExternalResourceResolver.of(getTransport(), evaluatorId);
}
private MessageTransport getTransport() throws ExternalReaderProcessException {
synchronized (lock) {
if (closed) {
throw new IllegalStateException("External reader process has already been closed.");
}
if (process != null) {
if (!process.isAlive()) {
throw new ExternalReaderProcessException(
ErrorMessages.create("externalReaderAlreadyTerminated"));
}
assert transport != null;
return transport;
}
// This relies on Java/OS behavior around PATH resolution, absolute/relative paths, etc.
var command = new ArrayList<String>();
command.add(spec.executable());
if (spec.arguments() != null) {
command.addAll(spec.arguments());
}
View on GitHub (pinned to f3efcbfc9b)
Solutions
- Reorder code so all resolver/spec usage happens before close()
- Check your shutdown sequence: close the external reader only after all module and resource resolution is done
- Guard each access with an isOpen/closed check in the calling code
- Avoid sharing the reader across threads without synchronization on its lifecycle
Example fix
// before resolver.close(); var moduleResolver = reader.getModuleResolver(); // throws // after var moduleResolver = reader.getModuleResolver(); // ...use moduleResolver... resolver.close();
Defensive patterns
Strategy: type-guard
Validate before calling
if (externalReader.isClosed()) {
throw new IllegalStateException("Cannot resolve modules: external reader already closed");
}
var moduleResolver = externalReader.getModuleResolver(); Type guard
boolean readerUsable(ExternalReaderProcess r) {
return r != null && !r.isClosed(); // consult the public lifecycle API before use
} Try / catch
try {
var resolver = reader.getResourceResolver();
} catch (IllegalStateException e) {
if (e.getMessage().contains("already been closed")) {
// recreate the reader or skip work scheduled after shutdown
}
} Prevention
- Enforce a strict lifecycle: acquire resolvers first, close the reader last
- Use try-with-resources or a shutdown hook ordering that closes readers after evaluation completes
- Avoid sharing ExternalReaderProcess across threads without coordination
- Never reuse a closed reader; create a new one instead
When it happens
Trigger: Calling getModuleResolver(), getResourceResolver(), getModuleReaderSpec(), getResourceReaderSpec(), or anything else that reaches getTransport() after ExternalReaderProcess.close() has completed.
Common situations: Evaluator shutdown racing with module/resource loading, closing readers in a finally block while another thread still resolves imports, double-close followed by reuse.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- externalReaderAlreadyTerminated
- externalReaderFailure
- externalReaderFailure
- externalReaderFailure
- externalReaderFailure
AI-assisted analysis of apple/pkl@f3efcbfc9b (2026-09-08).
Data as JSON: /api/errors/13bd0a51ce51d541.
Report an issue: GitHub.