WakaStart
Using the API

Errors — Business Codes

Wakastart business error codes, exact messages that can be tested programmatically, and status mappings.

Version v1.03 min read

Errors — Business Codes

This chapter lists error messages with an exact, stable value that you can test programmatically. Read HTTP errors for the standard HTTP codes.


Strict messages (testable programmatically)

Some message values have an exact, stable value:

MessageStatusMeaning
"Token enrichi manquant ou invalide"401wakaToken missing or invalid
"Insufficient credits"402Credit quota exceeded
"La fonctionnalité invitation n'est pas activée pour ce customer"403invitation feature disabled
"Cle API manquante (header x-api-key)"401x-api-key header absent
"Cle API utilisateur invalide ou expiree"401User API key invalid, revoked, or expired
"Cle API invalide"401Unknown API key
"Invitation non trouvée"200 (valid: false)Unknown invitation token (via verify)
"Cette invitation a déjà été acceptée"200 (valid: false)Token already used
"Cette invitation a expiré"200 (valid: false)expiresAt < now()
"Cette invitation a été annulée"200 (valid: false)Status CANCELLED

Note: the verify messages always return 200 with { valid: false, errorMessage: "..." } — not an HTTP error code.


Invitation errors — exhaustive handling

typescript
// Handle the 4 error states of an invitation async function loadInvitation(token: string) { const res = await fetch(`/api/proxy/invitations/verify/${token}`); const { valid, invitation, errorMessage } = await res.json(); if (!valid) { switch (errorMessage) { case "Cette invitation a expiré": return { error: "expired", ui: "Ce lien d'invitation a expiré. Demandez un nouveau lien." }; case "Cette invitation a déjà été acceptée": return { error: "accepted", ui: "Ce compte existe déjà. Connectez-vous directement." }; case "Cette invitation a été annulée": return { error: "cancelled", ui: "Cette invitation a été annulée par l'administrateur." }; case "Invitation non trouvée": default: return { error: "not_found", ui: "Ce lien d'invitation est invalide." }; } } return { valid: true, invitation }; }

Rights errors — structured messages

403 errors related to rights contain structured information in message:

typescript
// Extract the required vs actual level from a 403 function parseAdminLevelError(message: string) { const match = message.match(/Requis: (\w+), Actuel: (\w+)/); if (match) { return { required: match[1], actual: match[2] }; } return null; } // Usage const error = parseAdminLevelError( "Niveau d'administration insuffisant. Requis: CustomerAdmin, Actuel: User" ); // → { required: "CustomerAdmin", actual: "User" }

Rate limit codes by endpoint

Some endpoints have specific rate limit messages that let you identify the endpoint concerned:

typescript
// Rate limit detection pattern function isRateLimitError(status: number, headers: Headers): boolean { return status === 429 && headers.has("Retry-After"); } function getRateLimitWait(headers: Headers): number { return parseInt(headers.get("Retry-After") ?? "2", 10) * 1000; }

Validation errors — parsing

For 400s with an array of messages, build a map of fields in error:

typescript
interface ValidationErrors { [field: string]: string[]; } function parseValidationErrors( messages: string | string[] ): ValidationErrors { const msgs = Array.isArray(messages) ? messages : [messages]; const result: ValidationErrors = {}; for (const msg of msgs) { // NestJS format: "fieldName must be ..." const firstSpace = msg.indexOf(" "); if (firstSpace > -1) { const field = msg.substring(0, firstSpace); const error = msg.substring(firstSpace + 1); result[field] = [...(result[field] ?? []), error]; } } return result; } // Usage with react-hook-form try { await createUser(dto); } catch (err) { if (err instanceof ApiError && err.status === 400) { const fieldErrors = parseValidationErrors(err.body.message); Object.entries(fieldErrors).forEach(([field, errors]) => { form.setError(field as any, { message: errors[0] }); }); } }

Conflict errors (409) — strategies

typescript
try { await createInvitation(dto); toast.success("Invitation envoyée"); } catch (err) { if (err instanceof ApiError && err.isConflict) { const msg = err.body.message as string; if (msg.includes("invitation") && msg.includes("PENDING")) { // Invitation already exists — offer to resend setConflictAction("resend"); } else if (msg.includes("utilisateur")) { // User already in the Customer toast.error("Cet email est déjà utilisé dans ce Customer."); } } }

Go further