DEV Community

Vin Lookup
Vin Lookup

Posted on

Mapping vPIC ErrorCode to User-Safe Messages Without Fake Success

NHTSA vPIC DecodeVinValues-style responses often carry error-oriented signals alongside field columns: an ErrorCode, ErrorText, or similar status text that explains incomplete or invalid decode outcomes. Those signals are useful for a free VIN decode UI. The trap is treating a soft error as success because Make or Model is non-empty -- or inventing a green "valid VIN" badge when the API reported a problem.

This post is about honest error UX: map real vPIC error signals to user-safe copy, never paint empty Make/Model as success, and never invent "valid VIN" from a soft error. Keep field display separate from status handling. (Timeouts and AbortSignal retries are separate; here the focus is ErrorCode / ErrorText mapping.)

What ErrorCode-style signals are (and are not)

vPIC error fields answer a narrow question: did the decode service report a problem, and what short code or text did it attach?

They do not answer:

  • Whether a title history, NMVTIS, or Carfax-style report would clear
  • Whether the physical vehicle matches the plate or seller story
  • Permission to show a green "VIN verified" badge for marketing
  • A free pass to invent Make/Model when those columns are blank
  • That a checksum-looking VIN is "good enough" despite an error token

An empty Make with a quiet ErrorCode is still not success. A populated Model next to a non-success ErrorCode is still partial -- show sourced fields and the error, never hide status.

Normalize empties, do not invent success

Treat blank ErrorCode / ErrorText as "no explicit error token," not proof of success. Only declare success when your mapping table says so and required identity fields are present. Never invent "valid VIN" from silence alone.

const EMPTY = new Set([
  "",
  "not applicable",
  "n/a",
  "na",
  "null",
  "none",
  "unknown",
]);

export type DecodeStatus =
  | { kind: "ok"; note: string | null }
  | { kind: "error"; userMessage: string; rawCode: string | null; rawText: string | null }
  | { kind: "incomplete"; userMessage: string };

const SUCCESS_CODES = new Set(["0", ""]);

export function viewDecodeStatus(fields: {
  ErrorCode?: string | null;
  ErrorText?: string | null;
  Make?: string | null;
  Model?: string | null;
}): DecodeStatus {
  const code = (fields.ErrorCode ?? "").trim();
  const text = (fields.ErrorText ?? "").trim();
  const make = (fields.Make ?? "").trim();
  const model = (fields.Model ?? "").trim();
  const makeOk = Boolean(make && !EMPTY.has(make.toLowerCase()));
  const modelOk = Boolean(model && !EMPTY.has(model.toLowerCase()));

  const codeEmpty = !code || EMPTY.has(code.toLowerCase());
  const textEmpty = !text || EMPTY.has(text.toLowerCase());

  if (!makeOk || !modelOk) {
    return {
      kind: "incomplete",
      userMessage:
        "VIN decode did not return Make and Model. This is not a successful decode.",
    };
  }

  if (!codeEmpty && !SUCCESS_CODES.has(code)) {
    return {
      kind: "error",
      userMessage: textEmpty
        ? `Decode reported an error (code ${code}). Results may be incomplete.`
        : `Decode reported an error: ${text}`,
      rawCode: code,
      rawText: textEmpty ? null : text,
    };
  }

  if (!textEmpty && !/^(ok|success|0)$/i.test(text)) {
    // Soft / informational text with identity fields present -- surface, do not hide.
    return {
      kind: "error",
      userMessage: `Decode note: ${text}`,
      rawCode: codeEmpty ? null : code,
      rawText: text,
    };
  }

  return { kind: "ok", note: null };
}
Enter fullscreen mode Exit fullscreen mode

The incomplete branch matters. Empty Make/Model must never become a success card with grey placeholders that look real.

Forbidden upgrades

Product pressure often asks for:

  1. Showing a green "Valid VIN" badge whenever ErrorCode is blank
  2. Treating any non-empty Make as success even when ErrorText warns
  3. Mapping unknown ErrorCode values to "minor warning -- proceed"
  4. Inventing friendly success copy when both ErrorCode and identity fields are empty
  5. Hiding ErrorText so the card looks cleaner for SEO screenshots

Refuse those. Surface user-safe messages. Keep raw code/text in logs or a details disclosure -- not as fake success.

export function assertNoFakeSuccess(moduleSource: string): void {
  const banned = [
    /valid vin verified/i,
    /checksum approved/i,
    /guaranteed authentic/i,
    /carfax cleared/i,
    /nmvtis clean/i,
  ];
  for (const re of banned) {
    if (re.test(moduleSource)) {
      throw new Error(
        `ErrorCode UX must not invent fake success: ${re}`,
      );
    }
  }
}

export type StatusBanner = {
  tone: "ok" | "warn" | "error";
  message: string;
};

export function statusBanner(status: DecodeStatus): StatusBanner {
  if (status.kind === "ok") {
    return {
      tone: "ok",
      message: "Decode completed with Make and Model from vPIC.",
    };
  }
  if (status.kind === "incomplete") {
    return { tone: "error", message: status.userMessage };
  }
  return { tone: "warn", message: status.userMessage };
}
Enter fullscreen mode Exit fullscreen mode

Never map kind: "error" or kind: "incomplete" to success. Soft errors stay warn/error; incomplete stays error.

UI copy that stays honest

Prefer:

  • "Decode completed with Make and Model from vPIC."
  • "VIN decode did not return Make and Model. This is not a successful decode."
  • "Decode reported an error: ..." with the sourced ErrorText when safe to show

Avoid:

  • "Valid VIN verified" from a blank ErrorCode alone
  • Hiding ErrorText while showing partial Make as a full success
  • Grey Make/Model placeholders that look like real catalog data when both were empty

Quick checks

import assert from "node:assert/strict";

const missing = viewDecodeStatus({});
assert.equal(missing.kind, "incomplete");
assert.equal(statusBanner(missing).tone, "error");

const soft = viewDecodeStatus({
  ErrorCode: "1",
  ErrorText: "Check Digit does not calculate properly",
  Make: "HONDA",
  Model: "ACCORD",
});
assert.equal(soft.kind, "error");
assert.equal(statusBanner(soft).tone, "warn");
assert.ok(!/valid vin verified/i.test(statusBanner(soft).message));

const ok = viewDecodeStatus({
  ErrorCode: "0",
  ErrorText: "",
  Make: "TOYOTA",
  Model: "CAMRY",
});
assert.equal(ok.kind, "ok");
assert.equal(statusBanner(ok).tone, "ok");

const emptyMake = viewDecodeStatus({
  ErrorCode: "0",
  Make: "",
  Model: "CAMRY",
});
assert.equal(emptyMake.kind, "incomplete");
Enter fullscreen mode Exit fullscreen mode

Review rule: ErrorCode mappers must not contain fake-success marketing phrases except in forbidding tests. Never promote empty Make/Model to success.

Takeaway

vPIC ErrorCode / ErrorText style signals are status, not decoration. Map them to user-safe messages, require Make and Model before calling a decode successful, and never invent a "valid VIN" badge from silence or soft errors. Your free VIN UI stays trustworthy when success means sourced identity fields without a conflicting error -- and every other case stays incomplete or warned.

I maintain VIN Lookup, a free VIN decode based on NHTSA data.

Top comments (0)