Skip to main content

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

_tag
'Panic'
required
Discriminator tag, always "Panic".
message
string
required
Description of what went wrong.
cause
unknown
Optional underlying cause of the panic (the original thrown value).
name
string
Error name, set to "Panic".
stack
string | undefined
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 MessageCause
"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

  1. Let it crash: Allow the Panic to propagate and crash the operation
  2. Fix the bug: The Panic indicates a defect in your code
  3. 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

ScenarioUseExample
User provides invalid inputResult.err()Validation failure
External service unavailableResult.err()Network timeout
Callback throws inside mapPanicBug in callback
Cleanup code throwsPanicResource disposal bug
Type guard failsPanicInvariant violation

Best Practices

  1. 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");
    }
    
  2. 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));
    });
    
  3. 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 */ });
      }
    });
    
  4. Validate assumptions: Use type guards and assertions
    function process(value: unknown) {
      if (typeof value !== "number") {
        panic("Expected number"); // Document invariant violations
      }
    }
    

See Also