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

  1. Reorder code so all resolver/spec usage happens before close()
  2. Check your shutdown sequence: close the external reader only after all module and resource resolution is done
  3. Guard each access with an isOpen/closed check in the calling code
  4. 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

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


AI-assisted analysis of apple/pkl@f3efcbfc9b (2026-09-08). Data as JSON: /api/errors/13bd0a51ce51d541. Report an issue: GitHub.