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.
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.
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.readFileresolves withnullfor a missing file — the normal first-run case.net.fetchresolves for every HTTP status, including 404 and 500. Checkres.statusyourself.
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.
// 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
| Message | Cause | Fix |
|---|---|---|
Access denied: app lacks read permission | No read key in the manifest. | Declare it — "read": [] is enough for sandbox-only apps. |
Access denied: app lacks write permission | No write key in the manifest. | Declare "write": [] or the folders you need. |
Access denied: path is outside the app's allowed directories | The 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 directory | The 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 allowed | The path contained .. segments. | Normalise user-supplied names before joining paths. |
Refusing to delete root or sandbox root | deleteDir targeted Home or your sandbox root. | Delete specific subfolders instead. |
AI
| Condition | Internal code | What to show the user |
|---|---|---|
| App installed without AI permission | ai_disabled | Hide AI features; ship an update that declares the permission. |
| Nobody signed in | auth_required | “Sign in to VibeOS to use AI.” |
| Plan does not include AI | upgrade_required | “AI features need a plan upgrade.” Keep the rest usable. |
| Out of AI credits | quota_exceeded | “You are out of AI credits for now.” |
| Too many requests | rate_limited | “Slow down for a moment,” then retry with backoff. |
Network
| Condition | Internal code | Notes |
|---|---|---|
| URL not in the allow-list, or no network permission | permission_denied | Users can revoke origins after install — expect this at any time. |
| Malformed URL or non-HTTP protocol | invalid_url | Validate URLs you build from user input. |
| No signed-in session | auth_required | The proxy needs a session. |
| Private/internal target, upstream failure, timeout or oversized response | network_error | The message carries the specific reason, e.g. blocked host or response too large. |
Transport
| Message | Cause |
|---|---|
VOS request timed out | The 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
| Limit | Value | Applies to |
|---|---|---|
| SDK call timeout | 30 seconds | Every window.vos method, including AI and network calls. |
| Upstream HTTP timeout | 15 seconds | net.fetch |
| Maximum response size | 5 MB | net.fetch |
| Network allow-list | Up to 32 entries, https/http only | Manifest permissions |
| Filesystem change debounce | About 200 ms host-side, 300 ms in fs.watch by default | fs.watch |
| AI tokens and requests | Governed by the signed-in user's plan — not queryable from an app | ai.chat |
| Icon data URI | Keep under a few hundred KB; a 256px square PNG is ideal | Manifest 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
// 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
// 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
// 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.