LLM::Chat::ExceptionText

NAME

LLM::Chat::ExceptionText - An exception's message, for any exception, without ever throwing

SYNOPSIS


use LLM::Chat::ExceptionText;

class X::Quiet is Exception { }      # no message of its own

my $loud  = X::AdHoc.new(payload => 'disk full');
my $quiet = X::Quiet.new;

say exception-message($loud);        # disk full
say exception-message($quiet).raku;  # Str  (undefined: there is none)

say exception-text($loud);           # disk full
say exception-text($quiet);          # X::Quiet

$quiet.message;                      # dies: Stub code executed

DESCRIPTION

In Rakudo the base Exception.message is a stub: its whole body is ..., the yada operator. An exception class that defines neither a message method nor a $.message attribute therefore has no message to give. What .message hands back is a Failure whose exception says "Stub code executed" (an X::StubCode from Rakudo 2026.08 on, an X::AdHoc before), and that Failure throws the moment anything uses it as text: interpolates it, stringifies it, calls .lc or .lines on it.

Such classes are not hypothetical. Cro::HTTP::Client raises Cro::HTTP::Client::PipelineClosedBeforeHeaders, which has no message, whenever a server closes the connection before sending response headers — a server restarting, a proxy dropping an idle connection, a worker killed mid-request. Any library can declare one; a test double often does.

The place this bites is a CATCH block. A handler that formats the exception it caught with .message throws a second exception from inside the handler, and that one is not caught by anything. In the main program it replaces the original error with a stub error; inside a start block the program prints Unhandled exception in code scheduled on thread N and exits. The request that should have failed takes the whole process with it.


start {
    my $res = await $client.post: $url, body => $body;
    # ...
    CATCH {
        default {
            # WRONG: dies again, from inside the handler, for any
            # exception without a message.
            $response.quit("{.^name}: {.message}");
        }
    }
}

.message // '' happens to defuse the stub's Failure, but it is no help against a message method that dies outright, and an empty string is a poor thing to report as an error.

These two subs read the message defensively, so the handler cannot fail:


use LLM::Chat::ExceptionText;

start {
    my $res = await $client.post: $url, body => $body;
    # ...
    CATCH {
        default {
            note "{.^name}: {exception-message($_) // '(no message)'}";
            $response.quit(exception-text($_));
        }
    }
}

Every .message this distribution calls on an exception it did not create goes through one of them, so a transport, hook or tool error whose exception has no message fails the request it belongs to — with the exception's class name as its error text — instead of the process.

WHICH ONE

  • exception-text when you need something to show: the error handed to .quit, the content of a failed tool result, a log line that does not already name the class. It is never undefined, so it can go straight into a Str:D. An exception without a message is reported by its class name, which is the most specific thing there is to say about it.

  • exception-message when it matters whether there is a message: matching on the text, or choosing a fallback of your own. The answer is an undefined Str when there is none, which is exactly what // wants:


# Classify by the text, treating "no message" as "matches nothing".
my $text = (exception-message($_) // '').lc;
my $class = $text.contains('refused') ?? 'connection' !! 'unknown';

# A log line that already names the class should not name it twice.
note "{.^name}: {exception-message($_) // '(no message)'}";

WHAT COUNTS AS NO MESSAGE

exception-message returns the message as a Str when there is one, and an undefined Str when:

  • .message returns an undefined value: Nil, a type object, or a Failure — the last being what the base stub returns. A Failure is marked handled on the way, so it does not resurface later as an "unhandled Failure" warning.

  • .message dies. The exception it throws is caught and discarded.

  • The message's own .Str throws, is undefined, or is not a Str.

A defined message that is not a Str is stringified (42 becomes "42"). An empty string is a message, and is returned as one.

Anything can be passed, not only an Exception: a value without a message method — a type object included — simply has no message. Neither sub ever throws.

SUBROUTINES

exception-message


sub exception-message(Mu $exception --> Str) is export

The message of $exception as a Str, or an undefined Str when it has none (see /WHAT COUNTS AS NO MESSAGE).

exception-text


sub exception-text(Mu $exception --> Str:D) is export

The message when there is one, the class name otherwise. Always a defined Str.

The name is the declared class, without roles mixed into the object at runtime. That matters because await rethrows every exception with X::Await::Died mixed in, so the exception a CATCH around an await sees has a .^name like Cro::HTTP::Client::PipelineClosedBeforeHeaders+{X::Await::Died}; its text is Cro::HTTP::Client::PipelineClosedBeforeHeaders.


class X::Quiet is Exception { }

my $p = Promise.new;
$p.break(X::Quiet.new);
try await $p;

say $!.^name;              # X::Quiet+{X::Await::Died}
say exception-text($!);    # X::Quiet

LLM::Chat v0.10.3

Simple framework for LLM inferencing

Authors

  • Matt Doughty

License

Artistic-2.0

Dependencies

Cro::HTTP::Client:ver<0.8.11+>:auth<zef:cro>Digest::SHA256::Native:ver<1.0.0+>:auth<zef:bduggan>JSON::Fast:ver<0.21.0+>:auth<zef:timo>Template::Jinja2:ver<0.3.0+>:auth<zef:apogee>Tokenizers:ver<0.3.0+>:auth<zef:apogee>UUID::V4:ver<1.0.0+>:auth<zef:masukomi>

Provides

  • LLM::Chat
  • LLM::Chat::Backend
  • LLM::Chat::Backend::KoboldCpp
  • LLM::Chat::Backend::Mock
  • LLM::Chat::Backend::OpenAICommon
  • LLM::Chat::Backend::OpenRouter
  • LLM::Chat::Backend::Response
  • LLM::Chat::Backend::Response::OpenRouter
  • LLM::Chat::Backend::Response::OpenRouter::Augment
  • LLM::Chat::Backend::Response::OpenRouter::Stream
  • LLM::Chat::Backend::Response::Stream
  • LLM::Chat::Backend::Settings
  • LLM::Chat::Conversation
  • LLM::Chat::Conversation::Message
  • LLM::Chat::Debug
  • LLM::Chat::ExceptionText
  • LLM::Chat::Retry
  • LLM::Chat::Retry::Exceptions
  • LLM::Chat::Template
  • LLM::Chat::Template::ChatML
  • LLM::Chat::Template::DeepSeekV4
  • LLM::Chat::Template::Gemma2
  • LLM::Chat::Template::Jinja2
  • LLM::Chat::Template::Llama3
  • LLM::Chat::Template::Llama4
  • LLM::Chat::Template::MistralV7
  • LLM::Chat::Test::Kit
  • LLM::Chat::TokenCounter
  • LLM::Chat::ToolLoop
  • LLM::Chat::WireJSON

The Camelia image is copyright 2009 by Larry Wall. "Raku" is a trademark of the Yet Another Society. All rights reserved.

Built with Podlite — the markup and publishing tools behind this site.