Skip to content

Error Handling

s edited this page Aug 14, 2026 · 1 revision

Error Handling

Generated bindings have two error paths:

The JavaScript call is wrong
    -> TypeError or RangeError

The call was accepted, but the operation failed
    -> SupernoteError with a stable code

That is the useful distinction. A TypeError or RangeError normally means the call site needs fixing. A SupernoteError describes something your plugin may need to handle while it is running.

Handle an async call

Every generated module exports SupernoteError and its codes:

import document, {SupernoteError} from 'document';

Put the call itself inside try:

async function loadPage(page: number): Promise<Uint8Array | null> {
  try {
    return await document.loadPage(page);
  } catch (error) {
    if (error instanceof TypeError || error instanceof RangeError) {
      // The call does not match index.d.ts. This is normally a bug.
      throw error;
    }

    if (error instanceof SupernoteError) {
      switch (error.code) {
        case 'RESOURCE_EXHAUSTED':
          // Stop submitting more work. Retry later if that is safe.
          return null;

        case 'CANCELLED':
        case 'FEATURE_CLOSED':
          // This operation is no longer useful.
          return null;

        case 'IMPLEMENTATION_ERROR':
        case 'INTERNAL':
          console.error(error.code, error.message);
          throw error;
      }
    }

    // Do not hide an unrelated JavaScript error.
    throw error;
  }
}

This catches both possible timings:

  • argument validation can throw before a Promise exists;
  • an accepted async operation can reject its Promise later.

This shorter form only handles the second case:

document.loadPage(page).catch(handleError);

If loadPage(page) throws while checking the arguments, .catch(...) is never reached.

TypeError and RangeError

The generated boundary checks the call before starting async work:

Problem Result
Wrong argument count synchronous TypeError
Wrong JavaScript type synchronous TypeError
Fractional value for an integer synchronous RangeError
Number outside the generated integer range synchronous RangeError

For example, int64 requires JavaScript bigint, bytes require Uint8Array, and initial V2 values do not accept null.

These are programming errors. The generator does not create a worker task or pending Promise for a call that does not match the generated TypeScript API.

SupernoteError codes

Once a call has been accepted, generated and runtime failures use one class:

export type SupernoteErrorCode =
  | 'RESOURCE_EXHAUSTED'
  | 'CANCELLED'
  | 'FEATURE_CLOSED'
  | 'IMPLEMENTATION_ERROR'
  | 'INTERNAL';

export class SupernoteError extends Error {
  readonly code: SupernoteErrorCode;
}

Use error.code when your code needs to make a decision. The message is for people and may change between generator versions.

Code What it means What to do
RESOURCE_EXHAUSTED The bounded worker queue or another runtime resource could not accept more work. Submit less work at once. Retry later only if repeating the operation is safe.
CANCELLED Cooperative cancellation won before normal completion. Treat the result as unavailable. The underlying native/JVM call may still be finishing safely in the background.
FEATURE_CLOSED The module session closed while the JavaScript runtime was still healthy. Stop work for that session. A newly opened session is a different session.
IMPLEMENTATION_ERROR Your C++, Kotlin, Java, or user-owned constructor failed unexpectedly. Log the context and fix or deliberately handle the implementation failure.
INTERNAL Generated/runtime machinery failed or found a broken invariant. Record the generator version and report a small reproduction.

Do not parse error.message, and do not depend on a C++ exception type or JVM exception class appearing in JavaScript.

Synchronous calls throw

A synchronous export has no Promise, so both kinds of failure throw directly:

try {
  const count = document.pageCount();
  useCount(count);
} catch (error) {
  if (error instanceof SupernoteError) {
    console.error(error.code, error.message);
  }
  throw error;
}

Application errors are yours

The built-in codes describe binding and runtime failures. They are not a full set of errors for your application.

Do not treat an IMPLEMENTATION_ERROR message as a stable value such as FILE_NOT_FOUND or INVALID_DOCUMENT. If JavaScript needs to act on one of those conditions today, expose it deliberately through your own API using the supported value types.

Advanced cases

Most modules can stop at the sections above. The details below matter when an error crosses a JavaScript realm, when C++ calls a generated internal JVM route, or when a runtime is being torn down.

Errors crossing a JavaScript boundary

With the normal generated import, error instanceof SupernoteError is supported. Class identity may be lost after serialization or when an error crosses into another JavaScript realm. In that case, validate the object and compare its code against the exported SupernoteErrorCode values.

The initial public contract does not include fields such as backend, nativeException, details, operationId, or featureId.

Handle generated internal C++ errors

A synchronous generated internal C++ call either returns its value or throws supernote::Error:

try {
  const auto page =
      supernote::internal::DocumentFeature::internalJvmPage(3);
  use_page(page);
} catch (const supernote::Error &error) {
  if (error.code() == supernote::ErrorCode::FEATURE_CLOSED) {
    return;
  }
  throw;
}

An internal async call reports one supernote::Result<T> while its owning session remains valid:

supernote::internal::DocumentFeature::internalJvmBlocking(
    3,
    [](supernote::Result<std::vector<std::byte>> result) {
      if (!result) {
        log_error(result.error().code(), result.error().what());
        return;
      }

      use_bytes(std::move(result).value());
    });

The callback runs on a Supernote-managed non-JavaScript context. It has no stable worker, UI, or serial-thread guarantee, so captured state must be thread-safe and the callback must never touch JSI.

The callback runs at most once. If its session closes before delivery, it may not run at all. Do not make it the only cleanup path for a resource that must always be released.

Teardown

Feature teardown can still report an error when the JavaScript runtime is healthy:

feature closes
    -> pending accepted Promises reject with FEATURE_CLOSED

Complete JavaScript-runtime teardown is different:

JS runtime shuts down
    -> generated code stops touching JSI
    -> late results are discarded

The Promise belongs to the disappearing JavaScript realm, so trying to reject it during runtime shutdown would be unsafe.

Reporting a failure

Include the error code and message, supernote-module --version, the smallest marked declaration that reproduces the problem, and the full output from:

supernote-module validate <module-name> --build --verbose

For device-only failures, also include the Supernote model, firmware, whether it failed on the first load or a same-process reload, and the related adb logcat lines. See Troubleshooting for the complete diagnostic checklist.

Clone this wiki locally