Overview
Panic represents an unrecoverable error that indicates a defect in user code, not a expected failure condition. When thrown, a Panic indicates that something fundamentally wrong has occurred, such as:
- A callback throwing inside a Result combinator
- Generator cleanup (finally blocks) throwing errors
- Resource disposal (Symbol.dispose/asyncDispose) failing
- Other violations of the Result abstraction’s invariants
Panic errors should never be caught or handled in application logic. They indicate bugs that must be fixed.
Class Definition
class Panic extends TaggedError("Panic")<{
message: string;
cause?: unknown;
}>() {}
Panic is a TaggedError with tag "Panic" and required message property.
Properties
Discriminator tag, always "Panic".
Description of what went wrong.
Optional underlying cause of the panic (the original thrown value).
Error name, set to "Panic".
Stack trace. If cause is an Error, includes “Caused by:” chain.
Creating Panics
Constructor
new Panic({ message: string, cause?: unknown })
const panic = new Panic({
message: "map callback threw",
cause: new Error("original error")
});
panic() Function
Helper function that creates and throws a Panic:
panic(message: string, cause?: unknown): never
import { panic } from "better-result";
function processValue(value: unknown) {
if (typeof value !== "number") {
panic("Expected number, got " + typeof value);
}
return value * 2;
}
When Panics Occur
Combinator Callbacks Throwing
When user callbacks in Result methods throw errors:
const result = Result.ok(42).map(() => {
throw new Error("oops"); // Throws Panic!
});
Result.ok(1).map(() => {
throw new Error("boom");
}); // Panic!
Generator Cleanup Throwing
When finally blocks throw during Result.gen execution:
const result = Result.gen(function* () {
try {
yield* Result.err("expected error");
} finally {
throw new Error("cleanup failed"); // Panic!
}
});
Panics from cleanup code indicate resource disposal failures - serious bugs that must be fixed.
Success Path Cleanup Throwing
Cleanup throwing even when the operation succeeded:
const result = Result.gen(function* () {
try {
const data = yield* fetchData();
return Result.ok(data);
} finally {
throw new Error("logging failed"); // Panic even though data fetch succeeded!
}
});
Result.try catch Handler Throwing
When the custom error handler itself throws:
const result = Result.try({
try: () => JSON.parse(invalid),
catch: (cause) => {
throw new Error("catch handler failed"); // Panic!
}
});
Generator Body Throwing Directly
When generator code throws before any yield:
const result = Result.gen(function* () {
throw new Error("immediate failure"); // Panic!
yield* Result.ok(1);
});
Resource Disposal Throwing
When Symbol.dispose or Symbol.asyncDispose throws:
const resource = {
value: 42,
[Symbol.dispose]() {
throw new Error("disposal failed"); // Panic!
}
};
const result = Result.gen(function* () {
using res = resource;
yield* Result.ok(res.value);
}); // Panic when disposal happens!
shouldRetry Predicate Throwing
When retry logic throws:
await Result.tryPromise(
() => fetchData(),
{
retry: {
times: 3,
delayMs: 100,
backoff: "exponential",
shouldRetry: (error) => {
throw new Error("predicate failed"); // Panic!
}
}
}
);
Type Guard
isPanic(value: unknown): value is Panic
Check if a value is a Panic instance:
import { isPanic } from "better-result";
try {
Result.ok(1).map(() => { throw new Error("boom"); });
} catch (e) {
if (isPanic(e)) {
console.error("Panic detected:", e.message);
console.error("Cause:", e.cause);
}
}
Error Messages
Panic errors include descriptive messages indicating what operation failed:
| Panic Message | Cause |
|---|
"map callback threw" | User callback in .map() threw |
"andThen callback threw" | User callback in .andThen() threw |
"generator cleanup threw" | Finally block threw during cleanup |
"generator body threw" | Generator threw before yielding |
"Result.try catch handler threw" | Custom catch handler threw |
"shouldRetry predicate threw" | Retry predicate threw |
"Unreachable: Err yielded in Result.gen but generator continued" | Internal invariant violation |
Stack Traces
Panic errors include full stack traces with cause chaining:
try {
Result.ok(1).map(() => {
throw new Error("original error");
});
} catch (e) {
if (e instanceof Panic) {
console.log(e.stack);
// Panic: map callback threw
// at ...
// Caused by:
// Error: original error
// at ...
}
}
JSON Serialization
const panic = new Panic({
message: "Something went wrong",
cause: new Error("root cause")
});
const json = panic.toJSON();
// {
// _tag: "Panic",
// name: "Panic",
// message: "Something went wrong",
// cause: {
// name: "Error",
// message: "root cause",
// stack: "..."
// },
// stack: "..."
// }
Handling Strategy
Do NOT catch Panics in application logic. They indicate bugs, not recoverable errors.
What to Do When Panic Occurs
- Let it crash: Allow the Panic to propagate and crash the operation
- Fix the bug: The Panic indicates a defect in your code
- Log and alert: In production, log Panics for investigation
// ✅ Good - let Panic crash, fix the bug
const result = Result.ok(data).map(processData);
// ❌ Bad - catching Panic hides bugs
try {
const result = Result.ok(data).map(processData);
} catch (e) {
if (isPanic(e)) {
// Don't do this!
return Result.err("something went wrong");
}
}
Production Error Monitoring
Log Panics for monitoring, but don’t try to recover:
process.on('uncaughtException', (error) => {
if (isPanic(error)) {
logger.fatal('Panic detected - bug in application', {
message: error.message,
cause: error.cause,
stack: error.stack
});
// Re-throw or exit - don't continue
process.exit(1);
}
});
Panic vs Expected Errors
| Scenario | Use | Example |
|---|
| User provides invalid input | Result.err() | Validation failure |
| External service unavailable | Result.err() | Network timeout |
| Callback throws inside map | Panic | Bug in callback |
| Cleanup code throws | Panic | Resource disposal bug |
| Type guard fails | Panic | Invariant violation |
Best Practices
-
Never catch Panics: Let them crash and fix the underlying bug
// ❌ Bad
try {
result.map(fn);
} catch (e) {
if (isPanic(e)) return Result.err("error");
}
-
Don’t throw in callbacks: Use
Result.err() for expected failures
// ❌ Bad - throws Panic
result.map(x => {
if (invalid(x)) throw new Error("bad");
});
// ✅ Good - returns Err
result.andThen(x => {
if (invalid(x)) return Result.err(new ValidationError(...));
return Result.ok(process(x));
});
-
Test cleanup code: Ensure finally blocks and disposal don’t throw
Result.gen(function* () {
try {
yield* operation();
} finally {
// Make sure this can't throw!
await cleanup().catch(() => { /* log but don't throw */ });
}
});
-
Validate assumptions: Use type guards and assertions
function process(value: unknown) {
if (typeof value !== "number") {
panic("Expected number"); // Document invariant violations
}
}
See Also