Error Handling & Troubleshooting
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
| Code | When | How to handle |
|---|---|---|
EXTENSION_NOT_FOUND | No Vim Connect extension responded to the handshake | Ask the provider to install the extension, or confirm the page is a supported EHR |
EXTENSION_UPDATE_REQUIRED | The installed extension is older than this SDK needs | Prompt the provider to update the extension |
HANDSHAKE_TIMEOUT | The extension did not answer the initial handshake in time | Retry once; then surface a connection error. Tune with handshakeTimeout |
CONNECTION_TIMEOUT | The SDK could not connect within the timeout | Ensure the extension is installed and the page is a supported EHR |
TIMEOUT | An individual request exceeded requestTimeout | Retry; a catalog operation may be slow. Default is 40s |
WRONG_CONTEXT | initVimSDK() was called from the wrong surface — e.g. the UI initializer in a worker | Use initWorkerVimSDK() in Worker Apps |
BRIDGE_INIT_ERROR | The message bridge to the extension failed to start | Reload the app frame |
INIT_ERROR | Initialization failed for a reason not covered above | Log the cause and surface a generic connection error |
NOT_CONNECTED | An SDK call was made before the connection was established | Await initVimSDK(), and waitUntilReady() before reading the manifest |
Tokens
| Code | When | How to handle |
|---|---|---|
MISSING_ACCESS_TOKEN | No accessToken was supplied and no token_endpoint was found in the page URL | Pass accessToken, or register a token endpoint |
TOKEN_REQUIRED | A call needed an access token that was never provided | Same as above |
INVALID_TOKEN_ENDPOINT | The token_endpoint parameter is not an HTTPS URL with a public hostname — it is a safety check, not a parse failure | Serve your token endpoint over HTTPS on a publicly resolvable host; localhost and private hosts are rejected |
MISSING_TOKEN_CODE | The token endpoint flow ran with no authorization code | Confirm the OAuth redirect carried code; codes are single-use |
TOKEN_FETCH_ERROR | The token endpoint call failed or returned no access_token | Check your endpoint's response shape and status |
TOKEN_VALIDATION_FAILED | The access token was rejected | Re-launch the app to obtain a fresh one; there is no refresh flow |
ID_TOKEN_UNAVAILABLE | getIdToken() was called but the SDK holds no id_token | Use the token endpoint flow, or pass idToken to initVimSDK() — see Token Lifecycle |
Context and writeback
| Code | When | How to handle |
|---|---|---|
ENTITY_NOT_IN_CONTEXT | Tried to read or update an entity that is not in the current context | Wait for a context change event before writing |
OPERATION_NOT_CONFIGURED | The operation is not available for this EHR system | Check getCapability() before attempting updates |
PERMISSION_REQUIRED | Attempted a disruptive update, or a disruptive API operation, without permission | Call requestPermission() first |
PERMISSION_DENIED | The 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 door | Widen the manifest scope — no prompt can satisfy this |
INVALID_FIELDS | Invalid field paths in a permission request or update | Verify field paths against the manifest |
INVALID_DATA | The 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 example | Send the shape in the message's example; error.details has path, expected, received and example |
INVALID_OPERATION | The operation name is not valid for this entity | Check getManifest().operations |
VALIDATION_ERROR | A call argument failed validation — an invalid event name, or a non-serializable or oversized payload | Fix the argument; launchPayload is capped at 20 KB |
NO_WORKER | sdk.appEvents.send() was called by a single-surface app with no Worker App to receive the event | Only send app events from an app that ships both surfaces |
Catalog and manifest
| Code | When | How to handle |
|---|---|---|
MANIFEST_NOT_AVAILABLE | getManifest() was called before the SDK was ready | Await waitUntilReady() |
NOT_EXISTS | requestPermission('<method>') named a method that is not in your catalog. Note that calling an ungranted method throws a TypeError instead — see below | Check getManifest().operations |
NOT_IMPLEMENTED | The operation exists but is not implemented for this EHR | Use getManifest().operations to check availability |
MISSING_REQUIRED_PARAMETER | A required parameter was omitted | Check 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 onundefined. This holds for a single ungranted method inside a namespace you do have. A handler matchingerr.code === 'NOT_EXISTS'will never fire here. NOT_EXISTScomes fromrequestPermission('<method>'), and only there: it looks the method up in the catalog and throwsOperation "<namespace>.<method>" does not exist in API catalogwhen it is absent.PERMISSION_DENIEDis 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 repliesOperation "<namespace>.<method>" is not granted to this app. No prompt can satisfy either — the manifest has to change.PERMISSION_REQUIREDis a not-yet, not a no. A disruptive writeback, and a disruptive API operation, both need a grant first. Callsdk.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:
- Verify the Chrome extension is installed and enabled at
chrome://extensions - Confirm you're on a supported EHR page
- Try reloading the page
- 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:
- Always launch through the Vim Hub — don't navigate directly to
/launch - 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_idorclient_secret - Mismatched
redirect_uri(must exactly match the registered URL) - Using staging credentials against production backend, or vice versa
Solutions:
- Verify
NEXT_PUBLIC_CLIENT_IDandCLIENT_SECRETmatch your app registration - Ensure
NEXT_PUBLIC_ENVmatches the environment your app is registered in - Check that your
redirect_uriexactly matches the registered callback URL - 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:
- Context updates fire progressively — fields populate as the extension extracts them. Wait for subsequent updates.
- Check
sdk.ehr.getManifest().supportedEntitiesto 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:
- Check
getCapability('update')— ifavailableisfalse, the current screen doesn't support it - Verify the field path matches what the manifest reports as writable
- Check
getManifest().contextWritebackfor the entity'supdatableFieldslist
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