SDK reference

Errors & limits

Every SDK call can fail: a permission was declined, a file vanished, an API timed out. This page is the complete failure model — what rejections look like, what the platform caps are, and how to handle both without ruining the experience.

30s call timeout5 MB response caperr.code is not delivered

The rejection model

All SDK methods return Promises that either resolve with data or reject with a standard Error. Nothing throws synchronously, and no method returns an error object instead of throwing — with one deliberate exception, ai.canUse, which resolves with { ok, code } because “not entitled” is a normal answer rather than a fault.

javascript
try {
  await window.vos.fs.writeFile(dir, "notes.json", payload);
} catch (err) {
  // err is a standard Error.
  // err.message  → a human-readable description from the host
  // err.name     → "Error"
  // err.code     → undefined (see the caveat below)
  console.warn(err.message);
}

Two resolutions worth knowing, because they are not errors:

  • fs.readFile resolves with null for a missing file — the normal first-run case.
  • net.fetch resolves for every HTTP status, including 404 and 500. Check res.status yourself.

The err.code caveat

Branch on err.message, not err.code

The host classifies failures internally with codes such as auth_required and permission_denied, but errors cross the sandbox boundary as a message string. Inside your app, err.code is therefore undefined today, even for AI and network failures where codes are documented internally.

Write code that reads err.message and always has a generic fallback branch. If a future host release starts delivering err.code, message matching keeps working — so this is the safe long-term choice.

javascript
// Classify by message, with a generic fallback you are happy to show.
function classify(err) {
  const m = (err && err.message ? err.message : "").toLowerCase();
  if (m.includes("permission") || m.includes("access denied")) return "permission";
  if (m.includes("sign in") || m.includes("auth")) return "auth";
  if (m.includes("upgrade") || m.includes("plan")) return "upgrade";
  if (m.includes("quota") || m.includes("credit")) return "quota";
  if (m.includes("rate limit")) return "rate";
  if (m.includes("timed out") || m.includes("timeout")) return "timeout";
  if (m.includes("too large")) return "size";
  return "unknown";
}

Message catalogue

These are the failures a real app hits, grouped by namespace. Match loosely on substrings — exact wording can change between host releases.

Filesystem

MessageCauseFix
Access denied: app lacks read permissionNo read key in the manifest.Declare it — "read": [] is enough for sandbox-only apps.
Access denied: app lacks write permissionNo write key in the manifest.Declare "write": [] or the folders you need.
Access denied: path is outside the app's allowed directoriesThe path is not covered by the approved grant.Use the sandbox path, or request the folder and ship an update.
Access denied: path must be inside Home directoryThe path did not start with home.Build paths from Home-relative strings only.
Access denied: path is reserved.system, .Trash or another app's sandbox.Never touch reserved paths.
Access denied: path traversal is not allowedThe path contained .. segments.Normalise user-supplied names before joining paths.
Refusing to delete root or sandbox rootdeleteDir targeted Home or your sandbox root.Delete specific subfolders instead.

AI

ConditionInternal codeWhat to show the user
App installed without AI permissionai_disabledHide AI features; ship an update that declares the permission.
Nobody signed inauth_required“Sign in to VibeOS to use AI.”
Plan does not include AIupgrade_required“AI features need a plan upgrade.” Keep the rest usable.
Out of AI creditsquota_exceeded“You are out of AI credits for now.”
Too many requestsrate_limited“Slow down for a moment,” then retry with backoff.

Network

ConditionInternal codeNotes
URL not in the allow-list, or no network permissionpermission_deniedUsers can revoke origins after install — expect this at any time.
Malformed URL or non-HTTP protocolinvalid_urlValidate URLs you build from user input.
No signed-in sessionauth_requiredThe proxy needs a session.
Private/internal target, upstream failure, timeout or oversized responsenetwork_errorThe message carries the specific reason, e.g. blocked host or response too large.

Transport

MessageCause
VOS request timed outThe host did not answer within 30 seconds. Usually an unusually large operation or a busy desktop.
Unknown VOS method: …You called a method this host version does not have — feature-detect before using a new API.

Platform limits

LimitValueApplies to
SDK call timeout30 secondsEvery window.vos method, including AI and network calls.
Upstream HTTP timeout15 secondsnet.fetch
Maximum response size5 MBnet.fetch
Network allow-listUp to 32 entries, https/http onlyManifest permissions
Filesystem change debounceAbout 200 ms host-side, 300 ms in fs.watch by defaultfs.watch
AI tokens and requestsGoverned by the signed-in user's plan — not queryable from an appai.chat
Icon data URIKeep under a few hundred KB; a 256px square PNG is idealManifest iconImage

Storage is the user's, not yours

Files you write count against the user's VibeOS storage. There is no per-app quota API, so be frugal: prune caches, avoid duplicating large blobs, and never write on every keystroke — debounce.

Defensive patterns

Wrap every call

javascript
// Never let one failed call take down the whole app.
async function safe(label, fn, fallback) {
  try {
    return await fn();
  } catch (err) {
    console.warn(label + " failed:", err.message);
    setStatus(friendlyMessage(classify(err)));
    return fallback;
  }
}

const settings = await safe("load settings", () => loadSettings(), DEFAULT_SETTINGS);

Stay inside the 30-second budget

javascript
// Every SDK call rejects after 30s. For long jobs, work in chunks
// and keep the user informed between them.
async function saveAll(files) {
  for (let i = 0; i < files.length; i++) {
    setStatus("Saving " + (i + 1) + " of " + files.length + "…");
    await window.vos.fs.writeFile(files[i].dir, files[i].name, files[i].body);
  }
  setStatus("Saved");
}

Boot without assuming anything

javascript
// A boot sequence that degrades instead of showing a blank window.
async function boot() {
  let info = null;
  try {
    info = await window.vos.app.getInfo();
  } catch (err) {
    // The SDK exists but the host did not answer — show the UI read-only.
    renderShell({ readOnly: true, notice: "Storage unavailable" });
    return;
  }

  renderShell({ readOnly: !info.permissions.hasWrite });
  if (info.permissions.hasWrite) await loadDocuments(info.sandboxPath);
}

Error UX that passes review

  • Never show a blank window. Render the shell first, then fill it — a failed load should leave a usable, explained state.
  • Translate, do not echo. “Access denied: path is reserved” means nothing to a user. Say what the app cannot do and what they can do about it.
  • Never lose user work. Keep edits in memory and retry, rather than discarding them because a write failed.
  • Do not silently substitute. If the user asked for an AI action and AI is unavailable, tell them — do not quietly fall back to something else.
  • Log to the console too. Reviewers and the IDE console both read it, which makes issues far quicker to resolve.

Check it before you submit

Run through the troubleshooting & FAQ page and the pre-submission checklist in submission & review.