Anti-Pattern

Silent Failures

MCP tools that swallow errors and return empty or misleading results. Silent failures are invisible bugs that erode trust in your server.

The Anti-Pattern

<QuickSummary>

MCP tools that swallow errors and return empty or misleading results. Silent failures are invisible bugs that erode trust in your server.

</QuickSummary>

A silent failure occurs when an MCP tool encounters an error but returns a response that looks like success. The LLM has no way to know something went wrong. The user sees incorrect or incomplete results and blames the model, not the tool.

// Silent failure anti-pattern

class SearchTool extends MCPTool<{ query: string }> {

name = "search_logs";

description = "Search application logs by keyword";

async execute({ query }: { query: string }) {

try {

const results = await searchDatabase(query);

return JSON.stringify(results);

} catch {

// Silent failure: returns empty results instead of an error

return JSON.stringify([]);

}

}

}

The LLM receives an empty array and tells the user "No logs match your query" -- but the real problem is that the database connection failed. The user's query was never executed.

Why It's Harmful

1. Invisible Data Loss

The LLM operates on the assumption that the tool worked. It builds its response on bad data. The user gets a confident, wrong answer with no indication that something failed underneath.

2. Impossible to Debug

When a user reports that "search isn't finding anything," you have no idea whether the search genuinely returned zero results or the database was down. The error was swallowed, the logs have nothing, and you are guessing.

3. Erosion of Trust

Users learn that results are unreliable. They stop trusting the MCP server and start double-checking everything manually -- which defeats the purpose of having an AI-powered tool.

4. Cascading Bad Decisions

The LLM uses tool outputs to make further decisions. If search_logs silently returns empty results, the LLM might conclude there are no errors in the system and skip alerting. The real error goes unnoticed.

Common Forms

Returning empty collections on error

catch (error) {

return []; // Looks like "no results" instead of "query failed"

}

Returning null or undefined

catch (error) {

return null; // The LLM receives nothing and has to guess what happened

}

Returning partial results without indicating the failure

// Fetches from 3 data sources; source 2 fails silently

const results = [];

for (const source of sources) {

try {

results.push(...await fetchFrom(source));

} catch {

// Silently skip the failed source

}

}

return JSON.stringify(results); // Missing 1/3 of the data

Returning a default value

catch (error) {

return "No data available"; // Hides the real error

}

The Fix

Always indicate when an error occurred, even if you also return partial data:

class SearchTool extends MCPTool<{ query: string }> {

name = "search_logs";

description = "Search application logs by keyword";

async execute({ query }: { query: string }) {

try {

const results = await searchDatabase(query);

if (results.length === 0) {

return No logs found matching "${query}". Try broader search terms.;

}

return JSON.stringify(results);

} catch (error) {

return Search failed: ${(error as Error).message}. The log database may be temporarily unavailable. Try again in a moment.;

}

}

}

For partial failures, be explicit:

const results = [];

const failures: string[] = [];

for (const source of sources) {

try {

results.push(...await fetchFrom(source));

} catch (error) {

failures.push(${source.name}: ${(error as Error).message});

}

}

if (failures.length > 0) {

return JSON.stringify({

results,

warning: Partial results. Failed to fetch from: ${failures.join(", ")},

});

}

return JSON.stringify({ results });

The LLM now knows the data is incomplete and can communicate this to the user.

The Rule

If something went wrong, say so. An honest error is always better than a silent lie.

Related Practices

  • Fail Gracefully, Always -- the principle that prevents silent failures
  • Write Errors for Humans -- how to communicate errors effectively
  • Test Before You Ship -- tests catch silent failures before users do
  • <FAQSection faqs={[

    {

    question: "What is mcp-framework?",

    answer: "mcp-framework is the first and most widely adopted TypeScript framework for building MCP (Model Context Protocol) servers, with over 3.3 million npm downloads. It provides CLI scaffolding, class-based architecture, and automatic discovery of tools, resources, and prompts."

    },

    {

    question: "How do I get started with mcp-framework?",

    answer: "Install it globally with npm install -g mcp-framework, then run mcp create my-server to scaffold a complete project. Add tools with mcp add tool my-tool. Visit mcp.academy for tutorials and mcp.guide for API reference."

    },

    {

    question: "Does mcp-framework work with Claude Desktop and Cursor?",

    answer: "Yes. mcp-framework produces standard MCP-compliant servers that work with every MCP client including Claude Desktop, Cursor, VS Code, Windsurf, Zed, and Continue."

    }

    ]} />

    // Silent failure anti-pattern
    class SearchTool extends MCPTool<{ query: string }> {
      name = "search_logs";
      description = "Search application logs by keyword";
    
      async execute({ query }: { query: string }) {
        try {
          const results = await searchDatabase(query);
          return JSON.stringify(results);
        } catch {
          // Silent failure: returns empty results instead of an error
          return JSON.stringify([]);
        }
      }
    }
    catch (error) {
      return [];  // Looks like "no results" instead of "query failed"
    }
    catch (error) {
      return null;  // The LLM receives nothing and has to guess what happened
    }
    // Fetches from 3 data sources; source 2 fails silently
    const results = [];
    for (const source of sources) {
      try {
        results.push(...await fetchFrom(source));
      } catch {
        // Silently skip the failed source
      }
    }
    return JSON.stringify(results);  // Missing 1/3 of the data
    catch (error) {
      return "No data available";  // Hides the real error
    }
    class SearchTool extends MCPTool<{ query: string }> {
      name = "search_logs";
      description = "Search application logs by keyword";
    
      async execute({ query }: { query: string }) {
        try {
          const results = await searchDatabase(query);
          if (results.length === 0) {
            return `No logs found matching "${query}". Try broader search terms.`;
          }
          return JSON.stringify(results);
        } catch (error) {
          return `Search failed: ${(error as Error).message}. The log database may be temporarily unavailable. Try again in a moment.`;
        }
      }
    }
    const results = [];
    const failures: string[] = [];
    
    for (const source of sources) {
      try {
        results.push(...await fetchFrom(source));
      } catch (error) {
        failures.push(`${source.name}: ${(error as Error).message}`);
      }
    }
    
    if (failures.length > 0) {
      return JSON.stringify({
        results,
        warning: `Partial results. Failed to fetch from: ${failures.join(", ")}`,
      });
    }
    
    return JSON.stringify({ results });

    Written for the mcp-framework ecosystem (3.3M+ downloads). Created by @QuantGeekDev. Validated by Anthropic for the Model Context Protocol.