Define a single “success contract” for each step: only report ok/success if the operation is actually accepted; for optional steps, convert failures into a structured soft-skip (but still record a diagnostic reason).
Apply these rules: 1) Optional work ⇒ soft-skip with reason
2) Required work ⇒ hard-fail
3) API writes ⇒ treat provider error channels as failures
userErrors), treat non-empty as a failure even if the HTTP/transport succeeded.Example (optional step soft-skip)
async function stageBrowser({ hasNode, skipReasonRef, SkipChromium }) {
if (!hasNode) {
skipReasonRef.value = 'Node.js unavailable; optional browser tooling skipped';
return { skipped: true };
}
try {
await InstallAgentBrowser({ SkipChromium });
return { skipped: false };
} catch (e) {
skipReasonRef.value = `agent-browser install failed: ${String(e?.message || e)}`;
// Do NOT rethrow: optional degradation
return { skipped: true };
}
}
Example (GraphQL mutation userErrors)
async function applyMutationOrFail(client, mutation, variables) {
const data = await gql(client, mutation, variables);
const result = data?.[/* mutation root field */] || data;
const userErrors = result?.userErrors || [];
if (userErrors.length) {
throw new Error(`Mutation rejected by API: ${JSON.stringify(userErrors)}`);
}
// Optionally verify returned fields match the approved inputs here.
return result;
}
Outcome: fewer false “ok:true” results, predictable graceful degradation for optional features, and consistent diagnostics when failures happen.