Valibot safeParse on a Sume job status: keep polling on bad data

Valibot's safeParse returns a result instead of throwing, so a malformed Sume status body can be logged and retried. A short schema and runnable code.

5 min readSume
All posts

The answer

Valibot's introduction says that apart from parse, which throws, it offers a non-exception-based API with safeParse. For a poll loop that is the better fit: a bad or surprising status body becomes a value you can log and skip, not an exception that kills the loop.

Define a small schema for just the fields you use from a Sume status response: terminal, result_ready and next_poll_after_seconds. Extra fields are not a problem.

The schema

Sume wraps success bodies in data. The OpenAPI marks next_poll_after_seconds as nullable, so the schema must allow null. This runs as written with npm i valibot and a TypeScript runner.

import * as v from 'valibot';

const Status = v.object({
  data: v.object({
    terminal: v.boolean(),
    result_ready: v.boolean(),
    next_poll_after_seconds: v.nullable(v.number()),
  }),
});
type Status = v.InferOutput<typeof Status>;

const body = JSON.parse(
  '{"data":{"terminal":false,"result_ready":false,"next_poll_after_seconds":3}}',
);
const r = v.safeParse(Status, body);
if (r.success) {
  const s: Status = r.output;
  console.log(s.data.next_poll_after_seconds);
} else {
  console.log('bad payload', r.issues.length);
}

What the loop does with a failure

If safeParse fails once, treat it as a transient problem: log the issues, wait a default interval and read again. If it fails several times in a row, stop and surface an error, since a persistent mismatch means the API or your schema has changed. Never treat a parse failure as proof that the job failed; the job may be fine.

Parse outcome and the right reaction (read 2026-10-03)
OutcomeReaction
success, terminal falseWait next_poll_after_seconds, read again
success, terminal true, result_ready trueFetch /result
success, terminal true, result_ready falseRead events; the job failed or was canceled
safeParse failsLog issues, retry a few times, then alert

Why not trust the types alone

A generated type describes the spec you fetched. A runtime schema checks what actually arrives, which matters when a proxy returns an HTML error page with a 200, or when the API adds a state. Both are cheap to guard against with a few lines at the boundary.

Keep the schema minimal

Validate only what you read. A schema that mirrors the whole response fails on harmless additions; a small one stays stable. Compare it to the spec now and then, as in the contract test post.

Where to put the parsed value

Return the parsed output from one function and have the rest of the code depend on that function only. If Sume adds a field, nothing changes. If it renames one you read, exactly one file breaks and the failure shows up in a test rather than in production.

Valibot's page highlights a small bundle size and type inference through InferOutput, which is why the same schema can serve as both your runtime check and your TypeScript type. You never maintain two definitions that can drift apart.

Also validate the create response with a second schema. It carries status_url, result_url and idempotency_hit; a missing URL there is a different kind of failure than a bad status, and deserves a clear error before you start a poll loop that cannot work.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume