this.fail

this.fail() ends a tool call with an error result instead of a return value. The model reads that result like any other, so a clear message lets it recover: try another id, ask the user, or stop. Pass a PublicMcpError, and its message reaches the model in development and in production alike.

this.fail(error)

Reference

this.fail(error)

Call this.fail() inside a tool's execute(). Nothing after it runs.

close-ticket.tool.ts
import { PublicMcpError, Tool, ToolContext, z } from "@frontmcp/sdk";
import { TicketStore } from "./ticket-store";

@Tool({ name: "close_ticket", description: "Close a support ticket", inputSchema: { id: z.string() } })
export class CloseTicket extends ToolContext {
  async execute({ id }: { id: string }) {
    const ticket = this.get(TicketStore).find(id);
    if (!ticket) this.fail(new PublicMcpError(`There's no ticket ${id}.`));
    if (ticket.status === "closed") this.fail(new PublicMcpError(`Ticket ${id} is already closed.`, "TICKET_CLOSED"));
    return this.get(TicketStore).close(id);
  }
}

See more examples below.

Parameters

ParameterTypeDescription
errorErrorWhat went wrong. Use a PublicMcpError, or one of its subclasses, for a message the model should read. Any other error is hidden in production.

Returns

never. this.fail() throws to end execute(), and TypeScript knows it: after if (!ticket) this.fail(…), ticket is no longer undefined.

What the client receives

In a tool, this.fail() produces a normal tools/call result, with isError: true:

{
  "content": [{ "type": "text", "text": "There's no ticket T-9." }],
  "isError": true,
  "_meta": {
    "errorId": "err_4418a34f3368d9dc",
    "code": "PUBLIC_ERROR",
    "timestamp": "2026-09-24T09:12:03.512Z",
    "stack": "PublicMcpError: There's no ticket T-9.\n    at GetTicket.execute (…)"
  }
}

errorId is new for every error, and stack is only sent in development. (Under MCP 2026-07-28, _meta also carries the server's io.modelcontextprotocol/serverInfo.) The text and the code depend on what you pass:

You pass_meta.codeText in developmentText in production
new PublicMcpError(message)PUBLIC_ERRORmessagemessage
new PublicMcpError(message, code)codemessagemessage
Another public error classits codeits messageits message
new InternalMcpError(message, code?)INTERNAL_ERROR, or codemessageInternal FrontMCP error. Please contact support with error ID: err_…
Any other error, like new Error(message)SERVER_ERRORmessage, then Original error: and the stackInternal FrontMCP error. Please contact support with error ID: err_…

Development means NODE_ENV isn't production. The Playground always runs in development; see what production sends.

Error classes

These are exported from @frontmcp/sdk. All but InternalMcpError are public: their message reaches the model in production. They're the ones you'll use with this.fail(); Error classes lists every class FrontMCP exports, and what each one looks like to a client.

Class_meta.codeText
PublicMcpError(message, code?, statusCode?)PUBLIC_ERROR, or codemessage. statusCode (default 400) isn't part of a tool result.
InvalidInputError(message?, details?)INVALID_INPUTmessage (default "Invalid input: validation failed"), then Details: and details as JSON, if given. It's the code FrontMCP uses when arguments don't match inputSchema.
RateLimitError(retryAfter?)RATE_LIMIT_EXCEEDED"Rate limit exceeded", then ". Retry after 60 seconds" when retryAfter is 60.
QuotaExceededError(quotaType?)QUOTA_EXCEEDED"usage quota exceeded", with quotaType in place of "usage".
UnauthorizedError(message?)UNAUTHORIZEDmessage, default "Unauthorized".
ResourceNotFoundError(uri)RESOURCE_NOT_FOUND"Resource not found: " and the URI.
InternalMcpError(message, code?)INTERNAL_ERROR, or codemessage in development only.

Every one of them has an errorId and a code, and is an instanceof McpError.

In resources and prompts

ResourceContext and PromptContext have this.fail() too, and it works the same way: a public error keeps its message and code, and any other error is hidden in production. What changes is the shape. resources/read and prompts/get answer with a JSON-RPC error instead of a result, and the code that a tool puts in _meta.code is in error.data.code:

{
  "code": -32602,
  "message": "There's no ticket T-9.",
  "data": { "errorId": "err_fe0e63b17b92dc8f", "code": "PUBLIC_ERROR" }
}
You passcodemessagedata.code
new PublicMcpError(message, code?)-32602messagePUBLIC_ERROR, or code
new UnauthorizedError(message?)-32001messageUNAUTHORIZED
Any other error, like new Error(message)-32603message in development, Internal FrontMCP error. Please contact support with error ID: err_… in productionSERVER_ERROR

Throwing a public error gives the same result. A thrown error that isn't public is wrapped: its message follows Resource "tickets://T-9" read failed: or Prompt execution failed: , and data.code is RESOURCE_READ_ERROR or PROMPT_EXECUTION_FAILED.

Caveats

  • this.fail() works by throwing. A try/catch around it catches it, as a FlowControl, and the call carries on. See A tool call succeeds after this.fail().
  • Throwing works too. A PublicMcpError thrown from execute(), or from a provider it calls, reaches the client as if you had passed it to this.fail(). Only errors that aren't public come out differently: TOOL_EXECUTION_ERROR when thrown, SERVER_ERROR through this.fail(). See Failing vs throwing.
  • Every failure is in the server log, with its error ID. A public error is logged as a warning with the tool's name, the error ID and the code; any other error as an error, with its original message and stack. So an ID a user reports from production can be looked up.
  • this.fail() is protected: call it from inside the tool class. Code elsewhere, like a provider, throws a PublicMcpError instead.
  • In a resource or a prompt, this.respond() ends the call with a result, as this.fail() ends it with an error: the value is sent as a returned one would be, an object as JSON text and a string as text. (Changed in 1.9.3: before, it failed with Prompt output not found, or Resource output not found.) An execute() that returns nothing still fails that way.

Usage

Failing with a message the model can read

Say what went wrong and what to do instead. The call ends there, and the model gets your message as the result.

Open
import { PublicMcpError, Tool, ToolContext, z } from "@frontmcp/sdk";

const tickets = [
  { id: "T-1", title: "Cannot log in", status: "open" },
  { id: "T-2", title: "Invoice total is wrong", status: "closed" },
];

@Tool({
  name: "get_ticket",
  description: "Get one support ticket by id",
  inputSchema: { id: z.string().describe("Ticket id, like T-1") },
})
export class GetTicket extends ToolContext {
  async execute({ id }: { id: string }) {
    const ticket = tickets.find((t) => t.id === id);
    if (!ticket) this.fail(new PublicMcpError(`There's no ticket ${id}. Search by title with search_tickets.`));
    return ticket;
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

Choosing an error code

_meta.code lets a client, or your tests, tell failures apart without parsing the text. Give PublicMcpError a code of your own, or use a class that has one.

Error codes

Example 1 of 3

Your own code

Open
import { PublicMcpError, Tool, ToolContext, z } from "@frontmcp/sdk";

const tickets = [
  { id: "T-1", title: "Cannot log in", status: "open" },
  { id: "T-2", title: "Invoice total is wrong", status: "closed" },
];

@Tool({ name: "close_ticket", description: "Close a support ticket", inputSchema: { id: z.string() } })
export class CloseTicket extends ToolContext {
  async execute({ id }: { id: string }) {
    const ticket = tickets.find((t) => t.id === id);
    if (!ticket) this.fail(new PublicMcpError(`There's no ticket ${id}.`, "TICKET_NOT_FOUND"));
    if (ticket.status === "closed") this.fail(new PublicMcpError(`Ticket ${id} is already closed.`, "TICKET_CLOSED"));
    ticket.status = "closed";
    return ticket;
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

Failing vs throwing

For a PublicMcpError, throw and this.fail() give the same result: find_ticket throws, get_ticket fails, and the model reads the same message and code from both. They differ for errors that aren't public. sync_ticket throws a plain Error, which arrives as TOOL_EXECUTION_ERROR, with Tool "sync_ticket" execution failed: before the message; passed to this.fail(), the same error would be SERVER_ERROR. Production hides both.

Open
import { PublicMcpError, Tool, ToolContext, z } from "@frontmcp/sdk";

const tickets = [{ id: "T-1", title: "Cannot log in", status: "open" }];

@Tool({ name: "find_ticket", description: "Get one support ticket by id", inputSchema: { id: z.string() } })
export class FindTicket extends ToolContext {
  async execute({ id }: { id: string }) {
    const ticket = tickets.find((t) => t.id === id);
    if (!ticket) throw new PublicMcpError(`There's no ticket ${id}.`);
    return ticket;
  }
}

@Tool({ name: "get_ticket", description: "Get one support ticket by id", inputSchema: { id: z.string() } })
export class GetTicket extends ToolContext {
  async execute({ id }: { id: string }) {
    const ticket = tickets.find((t) => t.id === id);
    if (!ticket) this.fail(new PublicMcpError(`There's no ticket ${id}.`));
    return ticket;
  }
}

@Tool({ name: "sync_ticket", description: "Copy a ticket to the CRM", inputSchema: { id: z.string() } })
export class SyncTicket extends ToolContext {
  async execute({ id }: { id: string }) {
    throw new Error(`connect ECONNREFUSED 10.0.4.12:5432 while syncing ${id}`); // 🚩 not public
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

Errors from providers and other code

Providers and helpers can't call this.fail(), so they throw. A PublicMcpError they throw passes through the tool unchanged, and a subclass of it can carry its own code, so the tool doesn't need a try/catch:

Open
import { Tool, ToolContext, z } from "@frontmcp/sdk";
import { TicketStore } from "./ticket-store";

@Tool({ name: "reopen_ticket", description: "Reopen a closed support ticket", inputSchema: { id: z.string() } })
export class ReopenTicket extends ToolContext {
  async execute({ id }: { id: string }) {
    return this.get(TicketStore).reopen(id);
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

Anything else a provider throws, like a database driver's error, is wrapped as TOOL_EXECUTION_ERROR and hidden in production, like the plain Error in Failing vs throwing.

What production hides

The Call tab shows what a plain Error gives in development: its message and stack, which can include hostnames, queries and secrets. With NODE_ENV=production, FrontMCP sends an error ID instead. The Playground can't switch to production, so the test formats the same errors with createErrorHandler(). Its handle() uses the function that formats tools/call errors, and isDevelopment: false is what NODE_ENV=production means to it.

Open
import { Tool, ToolContext, z } from "@frontmcp/sdk";

@Tool({ name: "sync_ticket", description: "Copy a ticket to the CRM", inputSchema: { id: z.string() } })
export class SyncTicket extends ToolContext {
  async execute({ id }: { id: string }) {
    this.fail(new Error(`connect ECONNREFUSED 10.0.4.12:5432 while syncing ${id}`)); // 🚩 not public
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

Say what the model can do about it in a PublicMcpError, and keep the details for your logs.

Failing in a resource or a prompt

this.fail() works in resources and prompts too. The client gets a JSON-RPC error with your message, and your code in data.code (see the table).

Other entries

Example 1 of 2

Resource

Open
import { PublicMcpError, ResourceContext, ResourceTemplate } from "@frontmcp/sdk";

const tickets = [{ id: "T-1", title: "Cannot log in", status: "open" }];

@ResourceTemplate({ name: "ticket", uriTemplate: "tickets://{id}", mimeType: "application/json" })
export class Ticket extends ResourceContext<{ id: string }> {
  async execute(uri: string, { id }: { id: string }) {
    const ticket = tickets.find((t) => t.id === id);
    if (!ticket) this.fail(new PublicMcpError(`There's no ticket ${id}.`, "TICKET_NOT_FOUND"));
    return { contents: [{ uri, mimeType: "application/json", text: JSON.stringify(ticket) }] };
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.


Troubleshooting

"Internal FrontMCP error. Please contact support with error ID: err_…"

The server runs in production, and the call failed with an error that isn't public: a plain Error passed to this.fail() (_meta.code SERVER_ERROR), an InternalMcpError, or any other error thrown from execute() (TOOL_EXECUTION_ERROR). Look the ID up in the server log: FrontMCP logs every failure with its ID, and for these errors, the original message and stack. Fail with a PublicMcpError for anything the model should read.

Tool "get_ticket" execution failed: …, with _meta.code TOOL_EXECUTION_ERROR

execute() threw an error that isn't public, like a plain Error, a TypeError from a library, or an InternalMcpError, so FrontMCP wrapped it. If the message is for the model, throw or fail with a PublicMcpError instead. See Failing vs throwing. If the error comes from a provider, throw a PublicMcpError there.

_meta.code is SERVER_ERROR

The tool called this.fail() with an error that isn't an McpError, like new Error(…) or a TypeError from a library. FrontMCP wraps it as a server error, which shows the stack in development and hides everything in production. Use PublicMcpError if the message is for the model.

A tool call succeeds after this.fail()

this.fail() throws to end execute(), and a try/catch around it caught that. Here the tool reports the store as unavailable, although the real problem is an unknown ticket:

Open
import { FlowControl, PublicMcpError, Tool, ToolContext, z } from "@frontmcp/sdk";

const tickets = [{ id: "T-1", title: "Cannot log in", status: "open" }];

@Tool({ name: "close_ticket", description: "Close a support ticket", inputSchema: { id: z.string() } })
export class CloseTicket extends ToolContext {
  async execute({ id }: { id: string }) {
    try {
      const ticket = tickets.find((t) => t.id === id);
      if (!ticket) this.fail(new PublicMcpError(`There's no ticket ${id}.`)); // 🚩 caught below
      ticket.status = "closed";
      return { closed: true, id };
    } catch (e) {
      return { closed: false, reason: "The ticket store is unavailable.", caught: e instanceof FlowControl };
    }
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

Call this.fail() outside the try, or let FlowControl through:

} catch (e) {
  if (e instanceof FlowControl) throw e; // ✅ this.fail() ends the call
  return { closed: false, reason: "The ticket store is unavailable." };
}

Resource "…" read failed: … or Prompt execution failed: …

A resource or a prompt threw an error that isn't public, so FrontMCP wrapped it: code -32603, with data.code RESOURCE_READ_ERROR or PROMPT_EXECUTION_FAILED. In production the message is only "Internal FrontMCP error" and an error ID.

Open
import { ResourceContext, ResourceTemplate } from "@frontmcp/sdk";

@ResourceTemplate({ name: "ticket", uriTemplate: "tickets://{id}", mimeType: "application/json" })
export class Ticket extends ResourceContext<{ id: string }> {
  async execute(uri: string, { id }: { id: string }): Promise<{ contents: { uri: string; text: string }[] }> {
    throw new Error(`connect ECONNREFUSED 10.0.4.12:5432 while reading ${id}`); // 🚩 not public
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

If the client should read the message, fail with a PublicMcpError, as in Failing in a resource or a prompt.

TypeScript says Property 'fail' is protected

this.fail() was called from outside the tool class, for example from a provider that was handed the tool. Throw a PublicMcpError there instead: it reaches the client as it is.