Error Handling & Troubleshooting

Open in Claude

Handle SDK errors and debug common issues

SDK Errors

The SDK throws SDKError for runtime and validation failures. Each error has a code and optional details. Narrow a caught error with isSDKError() — SDK errors come from the core SDK loaded at runtime, so SDKError is exported as a type only and instanceof SDKError does not compile:

import { isSDKError } from '@vimconnect/app-sdk';

try {
  await sdk.ehr.context.encounter.update({ subjective: { chiefComplaintNotes: 'Headache' } });
} catch (err) {
  if (isSDKError(err)) {
    console.error(`[${err.code}] ${err.message}`, err.details);

    switch (err.code) {
      case 'ENTITY_NOT_IN_CONTEXT':
        // No active encounter — wait for context
        break;
      case 'PERMISSION_REQUIRED':
        // Need to request permission first
        break;
      case 'OPERATION_NOT_CONFIGURED':
        // This EHR doesn't support this operation
        break;
    }
  }
}

Error Codes

Every SDK error is an SDKError carrying a code. Switch on code, not on the message.

Codes below are the SDKErrorCode union, plus NO_WORKER — which the runtime throws but the union does not yet declare, so TypeScript will not autocomplete it.

Connection and initialization

CodeWhenHow to handle
EXTENSION_NOT_FOUNDNo Vim Connect extension responded to the handshakeAsk the provider to install the extension, or confirm the page is a supported EHR
EXTENSION_UPDATE_REQUIREDThe installed extension is older than this SDK needsPrompt the provider to update the extension
HANDSHAKE_TIMEOUTThe extension did not answer the initial handshake in timeRetry once; then surface a connection error. Tune with handshakeTimeout
CONNECTION_TIMEOUTThe SDK could not connect within the timeoutEnsure the extension is installed and the page is a supported EHR
TIMEOUTAn individual request exceeded requestTimeoutRetry; a catalog operation may be slow. Default is 40s
WRONG_CONTEXTinitVimSDK() was called from the wrong surface — e.g. the UI initializer in a workerUse initWorkerVimSDK() in Worker Apps
BRIDGE_INIT_ERRORThe message bridge to the extension failed to startReload the app frame
INIT_ERRORInitialization failed for a reason not covered aboveLog the cause and surface a generic connection error
NOT_CONNECTEDAn SDK call was made before the connection was establishedAwait initVimSDK(), and waitUntilReady() before reading the manifest

Tokens

CodeWhenHow to handle
MISSING_ACCESS_TOKENNo accessToken was supplied and no token_endpoint was found in the page URLPass accessToken, or register a token endpoint
TOKEN_REQUIREDA call needed an access token that was never providedSame as above
INVALID_TOKEN_ENDPOINTThe token_endpoint parameter is not an HTTPS URL with a public hostname — it is a safety check, not a parse failureServe your token endpoint over HTTPS on a publicly resolvable host; localhost and private hosts are rejected
MISSING_TOKEN_CODEThe token endpoint flow ran with no authorization codeConfirm the OAuth redirect carried code; codes are single-use
TOKEN_FETCH_ERRORThe token endpoint call failed or returned no access_tokenCheck your endpoint's response shape and status
TOKEN_VALIDATION_FAILEDThe access token was rejectedRe-launch the app to obtain a fresh one; there is no refresh flow
ID_TOKEN_UNAVAILABLEgetIdToken() was called but the SDK holds no id_tokenUse the token endpoint flow, or pass idToken to initVimSDK() — see Token Lifecycle

Context and writeback

CodeWhenHow to handle
ENTITY_NOT_IN_CONTEXTTried to read or update an entity that is not in the current contextWait for a context change event before writing
OPERATION_NOT_CONFIGUREDThe operation is not available for this EHR systemCheck getCapability() before attempting updates
PERMISSION_REQUIREDAttempted a disruptive update, or a disruptive API operation, without permissionCall requestPermission() first
PERMISSION_DENIEDThe call targets something outside the scope your manifest was granted — a field, for a writeback, or the operation itself, for an API call refused at the extension's invoke doorWiden the manifest scope — no prompt can satisfy this
INVALID_FIELDSInvalid field paths in a permission request or updateVerify field paths against the manifest
INVALID_DATAThe update payload does not have the field's structure — e.g. a string where a list of { code } is expected, a list item missing code, or a scalar where an object is expected (and, for update, a dotted top-level key). The message names the field and gives a valid exampleSend the shape in the message's example; error.details has path, expected, received and example
INVALID_OPERATIONThe operation name is not valid for this entityCheck getManifest().operations
VALIDATION_ERRORA call argument failed validation — an invalid event name, or a non-serializable or oversized payloadFix the argument; launchPayload is capped at 20 KB
NO_WORKERsdk.appEvents.send() was called by a single-surface app with no Worker App to receive the eventOnly send app events from an app that ships both surfaces

Catalog and manifest

CodeWhenHow to handle
MANIFEST_NOT_AVAILABLEgetManifest() was called before the SDK was readyAwait waitUntilReady()
NOT_EXISTSrequestPermission('<method>') named a method that is not in your catalog. Note that calling an ungranted method throws a TypeError instead — see belowCheck getManifest().operations
NOT_IMPLEMENTEDThe operation exists but is not implemented for this EHRUse getManifest().operations to check availability
MISSING_REQUIRED_PARAMETERA required parameter was omittedCheck the method signature

Which error a missing permission gives you

Four different things happen depending on what is missing, and only one of them is an error code you can match on.

  • Calling a method you were not granted throws a TypeError. Your granted operations are what build the API surface — one wrapper function per operation — so an ungranted method is simply absent from its namespace and the call lands on undefined. This holds for a single ungranted method inside a namespace you do have. A handler matching err.code === 'NOT_EXISTS' will never fire here.
  • NOT_EXISTS comes from requestPermission('<method>'), and only there: it looks the method up in the catalog and throws Operation "<namespace>.<method>" does not exist in API catalog when it is absent.
  • PERMISSION_DENIED is a refusal, and it reaches both paths. A writeback throws it when the field is outside your granted scope. An API call gets it from the extension's invoke door, which checks the operation against the granted scope independently of the manifest and replies Operation "<namespace>.<method>" is not granted to this app. No prompt can satisfy either — the manifest has to change.
  • PERMISSION_REQUIRED is a not-yet, not a no. A disruptive writeback, and a disruptive API operation, both need a grant first. Call sdk.ehr.api.<namespace>.requestPermission('<method>').

Workflow events are different again: an event you were not granted is simply never delivered, with nothing thrown. Check getManifest().operations for what your app actually has.

Defensive Patterns

Check Capabilities Before Writing

Always check if an operation is available before attempting it:

const cap = sdk.ehr.context.encounter.getCapability('update');

if (!cap.available) {
  // cap.reason tells you why:
  // - 'not_in_context': no active encounter
  // - 'not_configured': this EHR doesn't support encounter updates
  console.log('Cannot update encounter:', cap.reason);
  return;
}

// Safe to proceed
if (cap.permissionState === 'granted' || !cap.disruptive) {
  await sdk.ehr.context.encounter.update(data);
}

Wrap Context Subscriptions

Not all context keys are available on every EHR. Wrap subscriptions in try-catch:

const manifest = sdk.ehr.getManifest();

for (const ctx of manifest.supportedContexts) {
  try {
    sdk.ehr.context.onChange(ctx.contextKey, (prev, curr) => {
      // Handle context change
    });
  } catch (err) {
    console.warn(`Could not subscribe to ${ctx.contextKey}:`, err);
  }
}

Guard Against Stale Async Results

If your component unmounts or the context changes while an async operation is in flight, the result may be stale:

useEffect(() => {
  let cancelled = false;

  const unsubscribe = sdk.ehr.context.onChange('chart_open:patient', async (prev, curr) => {
    if (!curr) return;

    const result = await sdk.ehr.api.patient.getPatient();
    if (cancelled) return; // component unmounted or context changed

    setPatientData(result.data);
  });

  return () => {
    cancelled = true;
    unsubscribe();
  };
}, [sdk]);

Troubleshooting

"SDK not ready" or Connection Timeout

Symptoms: initVimSDK() throws or hangs, CONNECTION_TIMEOUT error.

Causes:

  • The Vim Connect Chrome extension is not installed
  • The current page is not a supported EHR
  • The extension is disabled or crashed

Solutions:

  1. Verify the Chrome extension is installed and enabled at chrome://extensions
  2. Confirm you're on a supported EHR page
  3. Try reloading the page
  4. Check the browser console for extension errors

"Missing launch_id" on Launch Page

Symptoms: Your launch page shows an error about missing launch_id.

Causes:

  • Navigating to the launch page directly instead of through the Vim Hub
  • The app URL is misconfigured on the Vim platform

Solutions:

  1. Always launch through the Vim Hub — don't navigate directly to /launch
  2. Verify your app's launch URL is correctly configured on the Vim platform

OAuth Token Exchange Fails

Symptoms: 401 or 400 error from /app-auth/token.

Causes:

  • Invalid or expired authorization code (codes are single-use)
  • Wrong client_id or client_secret
  • Mismatched redirect_uri (must exactly match the registered URL)
  • Using staging credentials against production backend, or vice versa

Solutions:

  1. Verify NEXT_PUBLIC_CLIENT_ID and CLIENT_SECRET match your app registration
  2. Ensure NEXT_PUBLIC_ENV matches the environment your app is registered in
  3. Check that your redirect_uri exactly matches the registered callback URL
  4. Re-launch the app to get a fresh authorization code

Context Data is Empty

Symptoms: onChange fires but curr.fields is empty or missing expected data.

Causes:

  • The EHR hasn't finished loading the data yet
  • The field isn't available for this specific EHR system

Solutions:

  1. Context updates fire progressively — fields populate as the extension extracts them. Wait for subsequent updates.
  2. Check sdk.ehr.getManifest().supportedEntities to see which fields are available for the current EHR.

Writeback Has No Effect

Symptoms: update() returns success but the EHR doesn't change.

Causes:

  • The provider is on a read-only screen
  • The EHR requires specific navigation state for writeback
  • The field isn't writable for this EHR configuration

Solutions:

  1. Check getCapability('update') — if available is false, the current screen doesn't support it
  2. Verify the field path matches what the manifest reports as writable
  3. Check getManifest().contextWriteback for the entity's updatableFields list

Debug Mode

Enable debug logging to see all SDK communication:

const sdk = await initVimSDK({ debug: true });

This logs all messages between your app and the extension to the browser console, including:

  • Event dispatches
  • Context updates
  • API calls and responses
  • Permission requests

On this page