> ## Documentation Index
> Fetch the complete documentation index at: https://docs.apiyi.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Gemini Image Generation API Error Handling Guide

> The three key diagnostic indicators for gemini-3-pro-image-preview (Nano Banana Pro) generation failures, Google's content moderation policy, and consumer-friendly messaging strategies to help developers turn technical errors into actionable user prompts.

## Overview

`gemini-3-pro-image-preview` (i.e., Nano Banana Pro) enforces strict content safety controls and will reject non-compliant requests at multiple layers. A simple "generation failed" message doesn't help users understand the problem. Good error handling needs to:

* **Accurately identify the rejection reason** — distinguish between content violations, knowledge-base limits, and technical errors
* **Provide friendly user messaging** — turn technical errors into understandable explanations
* **Offer actionable suggestions** — tell users how to adjust their request to succeed
* **Retain complete technical details** — for developer debugging

<Info>
  When a request returns **HTTP 200 but no image**, this is usually a safety judgment made on Google's side. APIYI's transparent proxy simply forwards the result as-is — we want our customers to generate images successfully too. The detection and messaging logic must be implemented on your application side.
</Info>

## Google Content Moderation Policy (2026 Update)

Google's image generation uses a **two-layer safety mechanism**:

1. **Configurable filters**: cover four categories — harassment, hate speech, sexually explicit content, and dangerous content — adjustable via `safetySettings`
2. **Built-in protections**: always active for core harms (such as child safety) and **cannot be disabled via parameters**

Explicitly prohibited content includes: child sexual abuse and exploitation (CSAE), violent extremism/terrorism, non-consensual intimate imagery (NCII), self-harm, sexually explicit content, hate speech, and harassment and bullying.

<Warning>
  **In February 2026, after Nano Banana 2 launched, Google significantly tightened its policies around people and copyright**, adding/strengthening the following frequent rejection scenarios (data as of May 2026 (UTC+8)):

  * **Public figures / celebrities**: photorealistic, recognizable real people
  * **Face swap (faceswap)**
  * **Re-dressing / face-altering real people**
  * **Tampering with financial or order information**
  * **Well-known IP** (such as Disney, since January 23, 2026)
  * **Watermark removal** and **minor**-related content

  Still allowed: fictional characters, stylized portraits, and illustrated figures.
</Warning>

Google's official policy documents (please copy and visit them yourself):

* Generative AI Prohibited Use Policy: `policies.google.com/terms/generative-ai/use-policy`
* Generative content common errors reference: `ai.google.dev/api/generate-content`

## Three Core Diagnostic Indicators

Check in order of **priority, from highest to lowest**:

### 1. candidatesTokenCount (highest priority) ⭐

* **Location**: `response.usageMetadata.candidatesTokenCount`
* **Meaning**: the token count of candidate content generated by the API
* **Rule**: a value of `0` means the request was **rejected outright at the content moderation stage** — no candidate content was even generated. This is the strictest rejection.

```json theme={null}
{
  "candidates": null,
  "usageMetadata": {
    "promptTokenCount": 271,
    "candidatesTokenCount": 0,
    "totalTokenCount": 271
  }
}
```

### 2. finishReason (second priority)

* **Location**: `response.candidates[0].finishReason`
* **Rule**: any value other than `STOP` indicates an abnormal completion that requires special handling

The latest image-related `finishReason` values (note that the Nano Banana series added image-specific values with the `IMAGE_` prefix):

| finishReason                                      | Meaning                         | User-friendly message                                            |
| ------------------------------------------------- | ------------------------------- | ---------------------------------------------------------------- |
| `STOP`                                            | Normal completion               | -                                                                |
| `IMAGE_SAFETY`                                    | Output-side image safety filter | Content triggered the image safety policy                        |
| `PROHIBITED_CONTENT` / `IMAGE_PROHIBITED_CONTENT` | Prohibited content              | Content violates the safety policy and was rejected              |
| `SAFETY`                                          | Safety filter                   | Content triggered the safety filter                              |
| `RECITATION` / `IMAGE_RECITATION`                 | Citation/copyright restriction  | Content may involve a copyright issue                            |
| `IMAGE_OTHER` / `NO_IMAGE`                        | No image/other                  | No image could be generated; please adjust your prompt and retry |
| `MAX_TOKENS`                                      | Length exceeded                 | Content length exceeds the limit                                 |

### 3. Text rejection explanation (important)

* **Location**: `response.candidates[0].content.parts[].text`
* **Rule**: when `finishReason` is `STOP` but `parts` contains only `text` and no image data, the API has returned a **rejection explanation** rather than an image. The text may be in Chinese or English, for example:

```text theme={null}
我不能为你创建带有色情、不雅或冒犯性内容的图像。这违反了我们的安全政策。
I can't generate images that are sexually explicit.
```

## Error Scenario Quick Reference

| Scenario                     | Detection condition                                                               | Typical cause                                                                   |
| ---------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| Content moderation rejection | `candidatesTokenCount === 0`                                                      | Prompt/reference image contains sensitive content; earliest-stage rejection     |
| Rejection during generation  | `finishReason !== 'STOP'` and `parts` is empty                                    | Prohibited content, safety filter                                               |
| Text rejection explanation   | `finishReason === 'STOP'`, has text but no image                                  | Sexually explicit content, non-compliant request                                |
| Knowledge-base limit         | text mentions a future year (2026+) or an unreleased product                      | Knowledge base updated through January 2025                                     |
| Prohibited feature           | text contains keywords like `watermark`, `faceswap`, re-dressing, celebrity, etc. | Prohibited features such as watermark removal/face swap/re-dressing/celebrities |

## Handling Flow (Decision Order)

```text theme={null}
Receive API response
  ├─ ① candidatesTokenCount === 0 ─→ Content moderation rejection
  ├─ ② candidates is empty ─────────→ API format error (system issue)
  ├─ ③ finishReason !== 'STOP' ────→ Rejection during generation (check mapping table)
  ├─ ④ content.parts is empty ─────→ Empty content (handle like finishReason)
  ├─ ⑤ Iterate parts to collect text and images
  ├─ ⑥ Has image ──────────────────→ ✅ Return success
  └─ ⑦ No image but has text ──────→ Show rejection explanation (optional keyword detection)
        └─ No text ───────────────→ Generic error + keep full response
```

## Code Implementation (Core)

Combine the checks above into a single parsing function:

```javascript theme={null}
async function processGeminiResponse(data) {
  // ① Highest priority: rejected outright at the content moderation stage
  if (data.usageMetadata?.candidatesTokenCount === 0) {
    return {
      success: false,
      errorType: 'ZERO_CANDIDATES_TOKEN',
      userMessage: 'Your request was rejected during content moderation. Please revise it and try again.',
      devMessage: 'candidatesTokenCount: 0 - rejected by Google content moderation',
      rawResponse: data,
    };
  }

  // ② candidates is empty — usually a system/format issue
  if (!data.candidates || !data.candidates.length) {
    return {
      success: false,
      errorType: 'NO_CANDIDATES',
      userMessage: 'A system error occurred. Please try again later.',
      devMessage: 'candidates is null or an empty array',
      rawResponse: data,
    };
  }

  const candidate = data.candidates[0];

  // ③ finishReason is not STOP — rejected during generation
  if (candidate.finishReason && candidate.finishReason !== 'STOP') {
    const reasonMessages = {
      PROHIBITED_CONTENT: 'Content violates the safety policy and was rejected.',
      IMAGE_PROHIBITED_CONTENT: 'Content violates the safety policy and was rejected.',
      SAFETY: 'Content triggered the safety filter.',
      IMAGE_SAFETY: 'Content triggered the image safety policy.',
      RECITATION: 'Content may involve a copyright issue.',
      IMAGE_RECITATION: 'Content may involve a copyright issue.',
      NO_IMAGE: 'No image could be generated. Please adjust your prompt and try again.',
      IMAGE_OTHER: 'No image could be generated. Please adjust your prompt and try again.',
      MAX_TOKENS: 'Content length exceeds the limit.',
    };
    return {
      success: false,
      errorType: 'FINISH_REASON',
      finishReason: candidate.finishReason,
      userMessage: reasonMessages[candidate.finishReason] || `Request rejected: ${candidate.finishReason}`,
      devMessage: `finishReason: ${candidate.finishReason}`,
      rawResponse: data,
    };
  }

  // ④ content.parts is empty
  if (!candidate.content?.parts) {
    return {
      success: false,
      errorType: 'NO_PARTS',
      userMessage: 'Generation failed. Please try again.',
      devMessage: 'candidate.content.parts is empty',
      rawResponse: data,
    };
  }

  // ⑤ Iterate parts: ⚠️ always collect text first, then check thoughtSignature
  const images = [];
  const texts = [];
  for (const part of candidate.content.parts) {
    if (part.text && !part.text.startsWith('data:image/')) {
      texts.push(part.text);
    }
    if (part.inlineData?.data) {
      images.push(`data:${part.inlineData.mimeType};base64,${part.inlineData.data}`);
    }
  }

  // ⑥ Having an image means success
  if (images.length > 0) {
    return { success: true, images, texts };
  }

  // ⑦ No image but has text — show the rejection explanation
  if (texts.length > 0) {
    const textContent = texts.join('\n');
    return {
      success: false,
      errorType: 'TEXT_RESPONSE',
      userMessage: textContent,          // Use the text returned by the API directly
      detectedType: detectContentType(textContent),
      apiText: textContent,
      rawResponse: data,
    };
  }

  // ⑧ Fallback: never just say "unknown error"
  return {
    success: false,
    errorType: 'UNKNOWN',
    userMessage: 'Generation failed. Please check your prompt and try again.',
    devMessage: 'No image data or text response found',
    rawResponse: data,
  };
}
```

Smart keyword detection (optional, for more specific messaging):

```javascript theme={null}
function detectContentType(text) {
  const t = text.toLowerCase();
  const isRejection =
    t.includes("i can't generate") || t.includes('i cannot create') ||
    t.includes("i'm just a language model") || t.includes('我不能') || t.includes('无法生成');
  if (!isRejection) return null;

  if (t.includes('watermark')) return 'watermark_removal';
  if (t.includes('faceswap') || t.includes('face swap')) return 'faceswap';
  if (t.includes('sexually') || t.includes('explicit') || t.includes('色情') || t.includes('不雅')) return 'nsfw';
  return 'general_rejection';
}
```

<Warning>
  **The most common pitfall**: a part carrying `thoughtSignature` may still contain important `text`. Always **collect the text first, then decide whether to skip** — otherwise the rejection explanation is lost and users only see "generation failed."
</Warning>

## Consumer-Friendly Messaging

Design principles: **clear and concise, positive guidance, actionable, no blame**. Recommended templates:

```text theme={null}
❌ Content does not meet requirements
Your request contains inappropriate content, so an image cannot be generated.
💡 Suggestion: Use healthy, positive descriptions; avoid sensitive topics; revise your prompt and try again.

❌ Feature not yet supported
This feature (e.g., watermark removal/face swap) is not supported. Please try a different editing approach.

❌ Content out of scope
The content you mentioned may be beyond the AI's knowledge range (updated through January 2025).
💡 Suggestion: Use common objects/concepts and avoid referencing future products.
```

Tiered display recommendations:

* **Consumer users**: by default show only the friendly explanation + revision suggestion
* **Business / tool providers**: by default expand technical details (`finishReason`, `candidatesTokenCount`, etc.)
* **Developers**: provide an "expand/collapse" toggle to view the full JSON response

## Best Practices

1. **Check strictly by priority**: `candidatesTokenCount` → `finishReason` → `parts` → extract data → keyword detection
2. **Collect text before checking thoughtSignature** to avoid losing the rejection explanation
3. **Keep the full response**: development/testing tools should always save the raw JSON for troubleshooting
4. **Support Chinese and English rejection text**: Google may return Chinese or English, so keyword matching must cover both
5. **Graceful degradation**: give a specific message when smart detection succeeds; otherwise show the API text directly; otherwise use the friendly `finishReason` name; and only then fall back to a generic message
6. **Never show "unknown error"**: always include an actionable suggestion or the full response

## FAQ

<AccordionGroup>
  <Accordion title="Why does the same prompt sometimes succeed and sometimes fail?">
    Google's safety filtering has randomness and context dependence: reference image content and how the prompt is combined all affect the judgment. Try adjusting the wording or using more indirect phrasing.
  </Accordion>

  <Accordion title="How do I tell whether it's a content problem or a technical problem?">
    `candidatesTokenCount: 0` or `finishReason: PROHIBITED_CONTENT` → content problem; `Failed to fetch` or an HTTP error → technical problem; an API text explanation → usually a content problem.
  </Accordion>

  <Accordion title="How much technical information should consumer users see?">
    Tiered display: by default show the friendly explanation + revision suggestion; optionally expand technical details; in development mode show the full JSON response.
  </Accordion>

  <Accordion title="Do I need to write separate handling for every finishReason?">
    No. A mapping table plus a generic fallback is enough: `reasonMessages[finishReason] || ` then display the raw value.
  </Accordion>
</AccordionGroup>

## Related Reading

* [Nano Banana Series Pricing Overview](/en/api-capabilities/nano-banana-pricing)
* [Nano Banana Pro Generation Failure Compensation Plan](/en/api-capabilities/nano-banana-pro-guarantee)
* [Nano Banana OSS Group](/en/api-capabilities/nano-banana-oss-group)
