Debug logging for Nutrient Web SDK
Nutrient Web SDK includes opt-in debug logging for NutrientViewer.load(). When you enable it, the SDK prints a tagged console.log line for each initialization stage.
Use debug logging when a load hangs, throws an unexpected error, or never resolves. The console output shows the last stage the SDK reached, which helps you identify whether the issue comes from SDK initialization, worker loading, WebAssembly setup, document loading, rendering, or your application environment.
This guide explains how to:
- Enable debug logging for one load
- Enable debug logging globally
- Read the stage lines
- Interpret common failure patterns
- Decide what to include in a bug report
Enable debug logging
You can enable debug logging for one load() call or for every load after the SDK module evaluates.
Enable debug logging for one load
Pass logLevel: 'debug' or NutrientViewer.LogLevel.DEBUG in the configuration object you pass to NutrientViewer.load():
NutrientViewer.load({ document: "/example.pdf", container: "#nutrient-viewer", logLevel: NutrientViewer.LogLevel.DEBUG,});This setting applies only to that load() call. Use it when you can edit the call site and need logs for a specific viewer instance.
Enable debug logging globally
Set globalThis.NUTRIENT_LOG_LEVEL = 'debug' before the SDK module evaluates:
<script> window.NUTRIENT_LOG_LEVEL = "debug";</script><script src=".../nutrient-viewer.js"></script>For dynamic imports, set the global before importing the SDK.
window.NUTRIENT_LOG_LEVEL = "debug";await import("@nutrient-sdk/viewer");The global setting applies to every later load() call. It also prints a sentinel line when the SDK entry module starts evaluating. That sentinel helps diagnose failures that happen before your code reaches NutrientViewer.load().
Set the global variable before the SDK loads
Set the global variable before the SDK entry module evaluates. If you set it after the SDK has loaded, debug logging won’t start for the already evaluated module.
For example, pasting window.NUTRIENT_LOG_LEVEL = 'debug' into DevTools and then calling location.reload() won’t work. Reloading discards the global variable before the SDK module evaluates again.
To toggle the global variable from DevTools, use one of these approaches:
- Navigate to a URL with a query parameter you control, and read that parameter in a script tag that runs before the SDK script tag.
- Use a browser extension that injects the variable at page-load time.
Read the log lines
When debug logging is on, the browser console shows stage lines like these:
[Nutrient #1] load() entered[Nutrient #1] validating configuration[Nutrient #1] configuration validated[Nutrient #1] worker spawned[Nutrient #1] chunk fetched: standalone[Nutrient #1] backend selected: standalone[Nutrient #1] React mounted[Nutrient #1] WASM instantiated[Nutrient #1] document opened[Nutrient #1] load() resolved[Nutrient #1] initial render completeLine prefix and counter
Each per-load line uses a [Nutrient #N] prefix. #N increments with each NutrientViewer.load() call in the current page session.
Use the counter to distinguish concurrent loads. For example, loadTextComparison() runs two load() calls in parallel, so the console can show [Nutrient #1] and [Nutrient #2] lines interleaved.
The counter only indicates order within the current page session. A page reload resets it.
Stages
A successful load can include the following stages. The order can vary based on the backend and whether the browser has already cached the chunk.
| Stage | Meaning |
|---|---|
load() entered | Your code called NutrientViewer.load(). |
validating configuration | The SDK is about to run synchronous validation on the configuration object. |
configuration validated | Synchronous checks passed. These include legacy Edge guards, nonce format checks, and deprecation warnings. Container resolution, license validation, and backend-specific checks happen later. |
worker spawned | Any in-flight NutrientViewer.preloadWorker() call has settled. If you didn’t call preloadWorker(), the SDK spawns the worker during the next stage. |
chunk fetched: standalone | The SDK fetched the standalone backend chunk over the network for the first time. |
chunk cached: standalone | The SDK reused the standalone backend chunk from a previous load. |
chunk fetched: server | The SDK fetched the server backend chunk. |
backend selected: standalone | The SDK constructed the standalone backend instance. |
backend selected: server | The SDK constructed the server backend instance. |
React mounted | The SDK mounted the viewer’s React tree into the container. |
WASM instantiated | The SDK instantiated the WebAssembly module and can call into it. This stage applies to standalone mode. |
GdPicture initialized | The SDK initialized GdPicture for the current load. This stage appears only for Office document loads. |
document opened | The engine parsed the document. |
load() resolved | NutrientViewer.load() is about to return the Instance to your code. |
initial render complete | The first page finished painting to canvas. |
Keep these ordering details in mind:
chunk fetched: standalonechanges tochunk cached: standaloneon later standalone loads when the backend module has already been imported. The same applies afterNutrientViewer.preloadWorker()has run. Each standalone load shows one of those two stages.initial render completefires afterdocument opened, but its position relative toload() resolveddepends on the environment. In standalone mode, it can interleave with post-connect()setup. In server mode, it usually appears afterload() resolved.
Failed loads
If a load fails, the SDK prints a final failed during init line:
[Nutrient #1] failed during init { lastStage: 'WASM instantiated', error: PSPDFKitError: ... }lastStage shows the stage the SDK reached before the failure. The error field contains the original Error object, so DevTools can show the full stack trace.
Aborted loads
If an AbortSignal cancels a load, the SDK prints one final aborted by signal line.
[Nutrient #1] aborted by signal { lastStage: 'React mounted' }The SDK logs the abort once. It doesn’t print a separate failed during init line for the same load. It also suppresses teardown errors and stage lines from callbacks that finish after the abort.
SDK module evaluated sentinel
When you enable the global setting before the SDK loads, the SDK prints one additional line at the top of its entry module:
[Nutrient] SDK module evaluatedThis line doesn’t include a #N counter because it fires before any NutrientViewer.load() call. The SDK emits it before any other code in the entry module runs.
Use the sentinel to identify whether the SDK entry module evaluated:
- If you see
[Nutrient] SDK module evaluated, the SDK entry module evaluated. If loading still fails or hangs, inspect the per-load[Nutrient #N]stage lines that follow. - If you don’t see
[Nutrient] SDK module evaluated, the SDK entry module didn’t evaluate. Look upstream of the SDK. Common causes include build-tool errors, failed chunk loads, and dynamicimport()calls that never settle.
The sentinel helps expose silent failures where execution never reaches NutrientViewer.load().
Common failure patterns
The following patterns describe common console output and where to investigate next.
No logs appear
You set globalThis.NUTRIENT_LOG_LEVEL = 'debug' before the SDK loads and reload the page, but the console remains empty. You don’t see the sentinel or any stage lines.
This means the SDK entry module didn’t evaluate. An upstream layer prevented the SDK from reaching its first executable line.
Check the following areas:
- Open the browser Network tab and confirm
nutrient-viewer.jsand its chunks loaded with a200status. - Check the console for parse errors or
SyntaxErrorentries that appeared before your script tag ran. Some bundlers can transform the UMD bundle into syntax older parsers reject. One known case is Turbopack UMD re-minification in Next.js 16, which can produce invalidnew Foo?.()optional-chain expressions that V8 rejects at parse time. - Confirm the dynamic
import()of the SDK resolved. If the import promise never resolves or rejects, a surroundingtry/catchwon’t run.
Logs stop and load never resolves
You see stage lines up to one point, and then logging stops. You don’t see failed during init, aborted by signal, or load() resolved.
This means an await in the SDK initialization flow is hanging without rejecting. Use the last visible stage to identify the boundary where initialization stopped.
Check the relevant area based on the last visible stage:
- Stopped at
worker spawned— The standalone backend chunk’s dynamic import is hanging. Check the Network tab for an in-flight request for thenutrient-viewer-standalone.*.jschunk, and confirm yourbaseUrlis reachable. - No stage lines after
[Nutrient] SDK module evaluated— A pendingNutrientViewer.preloadWorker()call may be hanging on the worker fetch, andload()is waiting for it. Confirm the worker file is reachable from the page’s origin. - Stopped at
WASM instantiated— The engine initialized, butdocument openeddidn’t fire. The document fetch is hanging. Confirm thedocumentURL responds. In server-backed deployments, confirmdocumentIdis valid and the JWT hasn’t expired. - Stopped at
document opened— The document parsed, but post-load setup is stuck. This setup can include the annotation provider, form fields, optional content group (OCG) layers, or embedded files. Check the Network tab for in-flight requests. - Stopped at
load() resolved—load()resolved, butinitial render completedidn’t fire. The React tree mounted, but canvas rendering is stuck. Confirm the container has nonzero dimensions and isn’tdisplay: none.
Read the failure stack trace
The failed during init line includes the original Error object. Expand it in DevTools to view the full stack trace.
Use the stack trace this way:
- Frames near the top usually come from Nutrient Web SDK.
- Frames near the bottom usually come from your application.
- The
messagefield usually gives the most actionable detail.
If the message reads Failed to initialize PSPDFKit: Invalid license key, check the license string, the activation domain, and whether the environment can reach the activation server.
If the message names a file, such as Couldn't fetch the file: Not Found, check the URL, server response, and cross-origin resource sharing (CORS) headers.
Performance impact
Debug logging is opt-in and off by default.
When logging is off, each stage call only performs one truthiness check. The SDK doesn’t allocate logging data, call the console, or produce console output.
When logging is on, the main cost comes from console.log calls and browser console rendering. You can leave the global setting enabled during a production debug session.
File a bug report
If debug logging identifies a failure pattern, include the full console output in your bug report. Useful output includes:
- A
failed during initline with the stack trace - A hung load with the last visible stage
- An absent SDK-module-evaluated sentinel
- Relevant Network tab failures
For more information, refer to the bug reporting guide.
Related guides
Use these guides with debug logging when you narrow down the failure area:
- For support requests and diagnostic attachments, refer to the bug reporting guide.
- For common integration and rendering issues, refer to the common issues guide.
- For license activation failures, refer to the license troubleshooting guide.
- For asset chunk loading failures, refer to the
ChunkLoadErrortroubleshooting guide. - For configuration options used by
NutrientViewer.load(), refer to theNutrientViewer.ConfigurationAPI reference.