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.
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);
}
}Parameters
| Parameter | Type | Description |
|---|---|---|
error | Error | What 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.code | Text in development | Text in production |
|---|---|---|---|
new PublicMcpError(message) | PUBLIC_ERROR | message | message |
new PublicMcpError(message, code) | code | message | message |
| Another public error class | its code | its message | its message |
new InternalMcpError(message, code?) | INTERNAL_ERROR, or code | message | Internal FrontMCP error. Please contact support with error ID: err_… |
Any other error, like new Error(message) | SERVER_ERROR | message, then Original error: and the stack | Internal 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.code | Text |
|---|---|---|
PublicMcpError(message, code?, statusCode?) | PUBLIC_ERROR, or code | message. statusCode (default 400) isn't part of a tool result. |
InvalidInputError(message?, details?) | INVALID_INPUT | message (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?) | UNAUTHORIZED | message, default "Unauthorized". |
ResourceNotFoundError(uri) | RESOURCE_NOT_FOUND | "Resource not found: " and the URI. |
InternalMcpError(message, code?) | INTERNAL_ERROR, or code | message 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 pass | code | message | data.code |
|---|---|---|---|
new PublicMcpError(message, code?) | -32602 | message | PUBLIC_ERROR, or code |
new UnauthorizedError(message?) | -32001 | message | UNAUTHORIZED |
Any other error, like new Error(message) | -32603 | message in development, Internal FrontMCP error. Please contact support with error ID: err_… in production | SERVER_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. Atry/catcharound it catches it, as aFlowControl, and the call carries on. See A tool call succeeds afterthis.fail().- Throwing works too. A
PublicMcpErrorthrown fromexecute(), or from a provider it calls, reaches the client as if you had passed it tothis.fail(). Only errors that aren't public come out differently:TOOL_EXECUTION_ERRORwhen thrown,SERVER_ERRORthroughthis.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()isprotected: call it from inside the tool class. Code elsewhere, like a provider, throws aPublicMcpErrorinstead.- In a resource or a prompt,
this.respond()ends the call with a result, asthis.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 withPrompt output not found, orResource output not found.) Anexecute()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.
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
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.
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:
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.
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
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:
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.
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.