Give-up codes
The four reasons a byte-target search can't reach its target, and what to offer instead.
target-unreachable is a negotiation, not a failure
When a { mode: "target" } search can’t put a file at or under the requested byte count,
runJob doesn’t throw and doesn’t silently hand back an oversized file. It resolves with
ok: false, and that result still carries a partial — the closest file the search actually
managed to produce, over target but real and downloadable — plus suggestions: the specific
levers that would make the target reachable.
{
ok: false,
error: { code: "target-unreachable", message: "…", reason: GiveUpCode },
partial?: EncodedImage, // the closest candidate found, if any
warnings: Warning[],
suggestions: Suggestion[],
}
error.reason is a GiveUpCode — the finer-grained “why” behind the flat target-unreachable
code. It only exists for this one ErrorCode; every other failure leaves reason undefined.
The four codes
GiveUpCode |
Meaning | Suggested lever |
|---|---|---|
FIXED_DIMENSIONS |
The quality floor is still over target and the caller pinned dimensions (allowDownscale: false). |
Allow downscaling. |
MIN_DIMENSION |
Downscaling was allowed, but the next hop would breach minDimension. |
Nothing left to shrink safely — try a different format, or raise the target. |
FORMAT_FLOOR |
Out of downscale hops, or shrinking stopped buying bytes: this format’s own entropy-plus-header floor is above the target. | Switch format (typically to WebP). |
BUDGET_EXHAUSTED |
Ran out of encode attempts before finding anything under target. A soft failure — nothing is structurally impossible, the search simply needs to run longer. | Call runJob again with a higher maxEncodes, or accept partial. |
This is exactly the same vocabulary the CLI’s exit codes 10–13 map to one-for-one — see Exit codes if you’re comparing the two surfaces.
Suggestion
type Suggestion =
| { kind: "allow-downscale" }
| { kind: "switch-format"; format: ImageFormat }
| { kind: "raise-target"; bytes: number }
| { kind: "accept-closest"; bytes: number }
| { kind: "lower-min-quality"; minQuality: number }
| { kind: "keep-searching"; additionalEncodes: number }
These are exactly the levers a “allow resizing” / “try WebP” banner would flip and resubmit — the
CLI’s own stderr sentences and --json suggestions array are built from this same union, worded
as text instead of structured data (see --json output).
lower-min-quality is declared in the type but nothing in this milestone emits it yet — kept
because widening a union must never silently drop a variant a future search change might start
using. Don’t build a UI branch that assumes every kind currently fires.
Using it
const result = await runJob(job, { file: bytes, backend })
if (!result.ok && result.error.code === "target-unreachable") {
console.log(`closest: ${result.partial?.sizeBytes} bytes`)
for (const s of result.suggestions) {
if (s.kind === "switch-format") console.log(`try format: ${s.format}`)
if (s.kind === "raise-target") console.log(`raise target to: ${s.bytes}`)
}
// offer result.partial to the user directly -- it's a real, complete file
}
isRecoverableCode("target-unreachable") (from the root import) returns true — of every
ErrorCode this engine defines, target-unreachable and dimension-exceeded are the two the
package itself considers negotiable in the product sense, as opposed to a hard failure like a bad
input or an encoder crash.