# Fixing {std,core}::panic!()
Both `std::panic!()` and `core::panic!()` are macro_rules which handle
the single-argument case specially, both in their own incompatible way.
This note discusses the problems and proposes changes to fix the inconsistencies,
both to prevent mistakes and confusion, and to make room for [RFC #2795].
(This document is a response to [this comment](https://github.com/rust-lang/rust/issues/67984#issuecomment-661308680).)
**UPDATE: This document is now outdated.
Latest version is in [the RFC](https://github.com/fusion-engineering-forks/rfcs/blob/panic/text/0000-panic-plan.md).**
## Current situation
In the case of `std::panic!(x)`, `x` does not have to be a string literal,
but can be of any (`Any + Send`) type.
This means that `std::panic!("{}")` and even `std::panic!(&"hi")` compile without errors or warnings,
even though these are most likely mistakes.
In the case of `core::panic!(x)`, `x` must be a `&str`, but does not have to
be a string literal, or even `'static`.
This means that `core::panic!("{}")` and `core::panic!(string.as_str())` compile
fine, which is somewhat surprising.
### Implicit formatting arguments
These inconsistencies become a much bigger problem once [RFC #2795] is implemented.
This RFC adds implicitly captured formatting arguments, as follows:
```rust=
let a = 4;
println!("a is {a}");
```
It modifies `format_args!()` to automatically capture variables that are named
in a formatting placeholder. See the RFC for details.
With the current implementations of `panic!()`, the result would be unfortunate:
```rust=
let a = 4;
println!("{}", a); // prints `4`
panic!("{}", a); // panics with `4`
println!("{a}"); // prints `4`
panic!("{a}"); // panics with `{a}` :(
```
### Overview
| Std panic call | Thrown value | Type (in a `Box<Any + Send>`) | notes
| ----------------------------------- | ------------- | ----------------------------- | -----
| `std::panic!("hello")` | `"hello"` | `&'static str` |
| `std::panic!("hello {}")` | `"hello {}"` | `&'static str` | 1
| `std::panic!("hello {}", 123)` | `"hello 123"` | `String` |
| `std::panic!(concat!("he", "llo"))` | `"hello"` | `&'static str` |
| `std::panic!(STATIC_STR)` | `"abc"` | `&'static str` |
| `std::panic!(string.as_str())` | Error: Need `'static` | |
| `std::panic!(123)` | `123` | `i32` | 2
| Core panic call | `fmt::Arguments` in `PanicInfo::message()` | notes
| ------------------------------------ | ------------------------------------------ | -----
| `core::panic!("hello")` | `format_args!("hello")` |
| `core::panic!("hello {}")` | `format_args!("hello {{}}")` | 1
| `core::panic!("hello {}", 123)` | `format_args("hello {}", 123)` |
| `core::panic!(concat!("he", "llo"))` | `format_args!("{}", "hello")` | 3
| `core::panic!(STATIC_STR)` | `format_args!("{}", "abc")` | 3
| `core::panic!(string.as_str())` | `format_args!("{}", string.as_str())` | 3
| `core::panic!(123)` | Error: Need `&str`
1: No warning or error about missing the formatting argument.
2: Works for any type that implements `Any + Send`.
3: These `fmt::Arguments` return `None` from their `as_str()` because they are not of the form `format_args!("...")`.
This means that showing these will have to pull in unecessary string formatting/padding code.
## Ideal situation
Ideally, the both panic macros would have equal behaviour, and
not handle the single-argument case differently.
They'd pass every invocation through `format_args!()` to allow for
implicit arguments, and checking for missing placeholders.
In addition, a new function `std::panic_any` would still allow
panicking with other types.
```rust
// core
macro_rules! panic {
() => (
$crate::panic!("explicit panic")
);
($($t:tt)*) => (
$crate::panicking::panic_fmt($crate::format_args!($($t)+))
);
}
// std
macro_rules! panic {
() => (
$crate::panic!("explicit panic")
);
($($t:tt)*) => (
$crate::rt::begin_panic_fmt(&$crate::format_args!($($t)+))
);
}
pub fn panic_any<M: Any + Send>(msg: M) -> ! {
crate::panicking::begin_panic(msg);
}
```
This will be a breaking change, as the behaviour will be different in some situations:
| Std panic call | Thrown value | Type (in a `Box<Any + Send>`) | Same as before
| ------------------------------------ | ------------- | ----------------------------- | ------------------
| `std::panic!("hello")` | `"hello"` | `String` or `&'static str`\* | :heavy_check_mark:
| `std::panic!("hello {}")` | - | Error: Missing argument | :x:
| `std::panic!("hello {}", 123)` | `"hello 123"` | `String` | :heavy_check_mark:
| `std::panic!(concat!("he", "llo"))` | `"hello"` | `String` or `&'static str`\* | :heavy_check_mark:
| `std::panic!(STATIC_STR)` | Error: Use `panic_any(STATIC_STR)` instead\*\*| | :x: \*\*\*
| `std::panic!(string.as_str())` | - | Error: Need `'static` | :heavy_check_mark:
| `std::panic!(123)` | Error: Use `panic_any(123)` instead | | :x:
| Core panic call | `fmt::Arguments` in `PanicInfo::message()` | Same as before
| ------------------------------------ | ------------------------------------------ | ------------------
| `core::panic!("hello")` | `format_args!("hello")` | :heavy_check_mark:
| `core::panic!("hello {}")` | Error: Missing argument | :x:
| `core::panic!("hello {}", 123)` | `format_args("hello {}", 123)` | :heavy_check_mark:
| `core::panic!(concat!("he", "llo"))` | `format_args!("hello")` | :heavy_check_mark:
| `core::panic!(STATIC_STR)` | Error: Need string literal, use `panic!("{}", ..)` | :x: \*\*\*
| `core::panic!(string.as_str())` | Error: Need `'static`, use `panic!("{}", ..)` instead | :x:
| `core::panic!(123)` | Error: Need `&str` | :heavy_check_mark:
\*: This can be `&'static str` if `begin_panic_fmt` checks `fmt::Arguments::as_str()`
before formatting into a `String`.
\*\*: Or `panic!("{}", STATIC_STR)`.
\*\*\*: This could work again if formatting is implemented as a const fn.
Although that has some interesting interactions with RFC 2795.
## How to get there
Unfortunately, we can't just break things :(
Three ways forward:
1. Tie this change to an edition.
`panic!()`s in Rust 2021 would behave slightly different than in Rust 2018.
Rust 2018 is left with inconsistencies in `panic!()`.
2. Slow deprecation path.
`panic!("{}")`, `panic!(non_literal)`, etc. start giving deprecation/lint warnings in all versions.
We provide good alternatives like `panic!("{}", ..)` and `panic_any(..)`.
After some months, we make the hard switch to the incompatible behaviour, hoping
that everyone acted upon the deprecation warnings and moved away from relying on the
behaviour we're now breaking.
3. A combination of both.
The deprecation/lint warnings are given in Rust 2018/2015, but the hard switch will only happen for Rust 2021.
This way, Rust 2018/2015 users will be made aware of potential mistakes and future incompatibilities,
but existing code will not be broken.
(Are there other good ways?)
All of these options have some implementation challenges:
- A (deprecation) warning/lint would require a custom lint implementation to trigger only on
string literals that `format_args!()` would not accept without extra arguments.
(Just the `panic` `macro_rules` macro cannot recognise `panic!(concat!("a", "b"))` is okay,
while `panic!(other_macro!())` is not.
Similarly, it can't distinguish between `panic!("ok")` and `panic!("bad {}")`.)
- Tying the behaviour of the panic macros to the edition requires them to be a
builtin macro (or at least use a builtin macro) which tracks down from which crate
it was called, and what edition that crate is using.
Note that we could pick different paths for different breakages. `panic!("{")` could follow path 2 while `panic!(STATIC_STR)` could follow path 1.
## How much would break
- `panic!("something containing a {")`
From these [grep.app results](https://grep.app/search?q=panic%21%5C%28%5Cs%2A%22%5B%5E%22%5D%2A%7B%5B%5E%22%5D%2A%22%5Cs%2A%5C%29®exp=true&filter[lang][0]=Rust),
it seems like using `{` in a panic message without formatting arguments rarely happens.
The only two cases that pop up in this search are clearly mistakes.
Breaking those is probably a good thing.
- `panic!(STATIC_STR)`
Also not used very often, but usages are valid and not mistakes: [grep.app results](https://grep.app/search?q=panic%21%5C%28%5Cs%2A%5BA-Z%5D%7B2%7D®exp=true&case=true&filter[lang][0]=Rust)
This comment also shows people do use this: [comment on #51999](https://github.com/rust-lang/rust/issues/51999#issuecomment-687590082).
- `panic!(some_expr)`
[grep.app results](https://grep.app/search?current=3&q=panic%21%5C%28%5Cs%2A%5B%5E%5Cs%22%29%5D®exp=true&filter[lang][0]=Rust)
Most of these are `panic!(format!(..))` (which should've been just `panic!(..)`) and other ways to produce a `String`, such as
`panic!(vec![..].join('\n'))` and `panic!(self.get_error())`.
Very few cases for `panic!(not_a_string)`.
Only one case of `core::panic!(non_static_str)`: In Miri's unit tests.
Although important to note here is that most embedded code (where this is most likely to be used) is probably not open source,
so many usages might stay hidden from searches like this.
[RFC #2795]: https://rust-lang.github.io/rfcs/2795-format-args-implicit-identifiers.html