{"slug": "every-ai-provider-fails-in-its-own-way-i-stopped-checking-status-codes-and-built", "title": "Every AI provider fails in its own way. I stopped checking status codes and built an error model instead.", "summary": "A developer built an API gateway routing between OpenAI, Anthropic, and Gemini, and discovered that each provider returns errors in different formats, making naive error handling unreliable. Instead of forwarding raw status codes, the developer created a normalized error model that classifies failures into a fixed set of categories (e.g., rate_limited, overloaded) regardless of the provider. This approach eliminates provider-specific special-casing and simplifies retry logic, and has been incorporated into the developer's AI gateway project, Apiarium.", "body_md": "I built an API gateway that routes between OpenAI, Anthropic and Gemini. I figured integrating both providers would be the hard part.\n\nIt wasn't. Calling their APIs is maybe an afternoon of work each. The hard part showed up later, the first time something went wrong.\n\nEarly on, my error handling was basically: catch whatever the provider throws, forward the status code, move on.\n\n```\n} catch (error) {\n  res.status(error.status || 500).json({ error: error.message })\n}\n```\n\nThis worked fine until I actually looked at what each provider sends back when something goes wrong.\n\nOpenAI wraps its errors in an object with a `type`\n\nand sometimes a `code`\n\n. Anthropic wraps its errors differently, with its own `type`\n\nfield that means something else entirely. A 429 from one provider might mean \"you're sending too fast, back off.\" A 429 from another context might mean something closer to \"we're out of capacity right now, this isn't really about your rate at all.\"\n\nIf you're just forwarding `error.status`\n\nand `error.message`\n\nstraight through, none of that nuance survives. Your own error handling ends up being provider-specific whether you meant it to be or not, because the *shape* of the failure is different depending on who you called.\n\nInstead of trusting each provider's raw error shape, every call now normalizes into the same internal error model before it reaches the response:\n\n``` js\n} catch (error) {\n  const classified = classifyProviderError(error)\n\n  res.status(classified.httpStatus).json({\n    error: 'AI provider error. Please try again.',\n    error_class: classified.error_class,\n    provider: classified.provider\n  })\n}\n```\n\n`error_class`\n\nis one of a small fixed set: `rate_limited`\n\n, `overloaded`\n\n, `quota_exceeded`\n\n, `invalid_request`\n\n, `authentication_error`\n\n, `server_error`\n\n. That's true regardless of which provider actually failed. The raw provider error still gets logged for me to debug, but what the caller sees is the *category* of failure, not the provider's specific wire format.\n\nThe point isn't that this retries anything automatically. It doesn't. It's that \"should I retry, and how\" becomes a decision you can make once, based on `error_class`\n\n, instead of once per provider you happen to be calling that day.\n\nI expected the differences between providers to be about capability, better reasoning, different context windows, that kind of thing. What actually ate my time was smaller and dumber: two APIs disagreeing about what a 429 even *means*.\n\nOnce you stop trusting the HTTP status code to tell you the whole story and start asking \"what actually happened here, independent of who I called,\" a lot of the provider-specific special-casing just goes away.\n\nIf you're building anything that calls more than one LLM provider, worth checking early: are you branching your error handling on the provider, or on the actual failure type? Those aren't the same question, and the difference only shows up once something breaks in production.\n\nThis eventually became part of [Apiarium](https://apiarium.dev), the AI gateway I'm building.\n\nNot because I wanted another abstraction layer.\n\nBecause I got tired of writing provider-specific error handling.", "url": "https://wpnews.pro/news/every-ai-provider-fails-in-its-own-way-i-stopped-checking-status-codes-and-built", "canonical_source": "https://dev.to/manolito99/every-ai-provider-fails-in-its-own-way-i-stopped-checking-status-codes-and-built-an-error-model-25do", "published_at": "2026-07-10 09:33:00+00:00", "updated_at": "2026-07-10 09:42:48.496446+00:00", "lang": "en", "topics": ["developer-tools", "ai-infrastructure", "ai-tools", "large-language-models"], "entities": ["OpenAI", "Anthropic", "Gemini", "Apiarium"], "alternates": {"html": "https://wpnews.pro/news/every-ai-provider-fails-in-its-own-way-i-stopped-checking-status-codes-and-built", "markdown": "https://wpnews.pro/news/every-ai-provider-fails-in-its-own-way-i-stopped-checking-status-codes-and-built.md", "text": "https://wpnews.pro/news/every-ai-provider-fails-in-its-own-way-i-stopped-checking-status-codes-and-built.txt", "jsonld": "https://wpnews.pro/news/every-ai-provider-fails-in-its-own-way-i-stopped-checking-status-codes-and-built.jsonld"}}