Skip to content

Error handling

Compiles are asynchronous, so nothing is thrown at you directly: compileString and compilePath never throw synchronously. Every failure — an engine error, a Sass error, a timeout, a cancellation, a closed compiler — arrives as a failed future.

try {
  var output = compiler.compileString(scss, options).get(30, TimeUnit.SECONDS);
  return output.getCss();
} catch (ExecutionException e) {
  if (e.getCause() instanceof JsassCompilationException sassError) {
    // a real Sass problem: report it to the developer
    return "/* " + sassError.getMessage() + " */";
  }
  throw e;
}

The exception hierarchy

Exception
└── JsassException                         checked, the root of everything jsass reports
    ├── JsassCompilationException          a compile failed
    │   ├── JsassCompilationTimeoutException      exceeded its timeout
    │   ├── JsassCompilationInterruptedException  worker thread was interrupted
    │   ├── SassFunctionExecutionException        a custom function threw
    │   └── SassImporterExecutionException        an importer threw
    └── AmbiguousImportException           one import matched several stylesheets

RuntimeException
├── SassPathTraversalException             an import escaped its allowed root
└── AmbiguousImportRuntimeException        unchecked carrier of an AmbiguousImportException

AmbiguousImportRuntimeException exists because the Importer SPI declares no checked exceptions: an importer that detects an ambiguous import (WebjarImporter does) throws it, and getCheckedCause() returns the typed AmbiguousImportException with every candidate.

Reading a Sass error

JsassCompilationException keeps the diagnostics dart-sass produced in structured form:

Getter Content
getSassMessage() dart-sass's own message, unmodified
getSourceSpan() a SourceSpan record: URL plus 0-indexed start/end line and column
getSassStack() the Sass-level stack trace, when there is one
getRawEngineStack() the raw stack trace reported by the JavaScript engine (V8, Node or GraalJS) — for debugging jsass itself; on the embedded engine, dart-sass's own formatted error with the source excerpt

getMessage() renders a sanitized summary: the message, the last path segment of the URL and a 1-indexed line and column. It never contains an absolute filesystem path or engine internals, which makes it safe to put in an HTTP response or a log that leaves the machine. When you want more, read the structured getters.

var span = e.getSourceSpan();
if (span != null) {
  log.error("{} at {}:{}:{}", e.getSassMessage(), span.url(),
      span.startLine() + 1, span.startColumn() + 1);
}

Timeouts and interrupts

Both are compilation failures, but they mean different things:

  • JsassCompilationTimeoutException — the compile ran longer than its effective timeout. getElapsed() returns the Duration that passed before the engine was terminated.
  • JsassCompilationInterruptedException — something interrupted the worker thread from the outside, typically an executor.shutdownNow() during application shutdown.

Neither carries a source span: there is no line of SCSS to blame.

Cancelling the future (future.cancel(true)) asks the engine to abort the running compile, and the future completes as cancelled rather than with one of the exceptions above.

The embedded engine cannot abort a single compile inside dart-sass. On a timeout or a cancel it fails the future at once and retires the dart-sass process, which takes no new compiles and ends once its other compiles are done; see one process per compiler.

When the embedded engine cannot start dart-sass at all — no executable found, a path that does not exist, a dart-sass too old for protocol 3 — the compile fails with a JsassCompilationException whose message names the cause: the sources it tried, the missing path, or the protocol version it found and the ones it supports.

Warnings and debug messages

@warn and @debug are not errors and never fail a compile. They reach you through a SassLogger on the options:

var options = StringOptions.builder()
    .logger(new SassLogger() {
      @Override
      public void warn(String message, Options opts) {
        log.warn("{}{}", opts.isDeprecation() ? "[deprecated] " : "", message);
      }

      @Override
      public void debug(String message, Options opts) {
        log.debug(message);
      }
    })
    .build();

The Options argument adds context: getSpan() locates the message in the source, isDeprecation() says whether it is a deprecation warning, and getStack() gives the Sass call stack when one is available. Without a logger, dart-sass's default handling applies.

If deprecation warnings are the problem rather than the signal, silenceDeprecations, quietDeps and fatalDeprecations on the options are the sharper tools.

Module resolution failures

On the JavaScript engines, when the sass module — or postcss / autoprefixer with autoprefixer(true) — cannot be resolved or loaded, the compile fails with a JsassCompilationException that names the specifier and says how to make it resolvable, instead of a generic engine error. The same happens when postcss or autoprefixer resolve to CommonJS rather than an ES module.

Engine-level logging

On the Javet engines, Javet's own diagnostics go to SLF4J by default — no setup needed; replace the bridge with JavetOptions.javetLogger on the compiler builder. This is a different channel from the SassLogger: it reports what the JavaScript engine itself is doing, not what your stylesheets say. Turn it up when you are debugging module resolution or an engine failure, not for stylesheet diagnostics. jsass's own classes log under io.bit3.jsass.