Try โ€‚โ€‰HackMD

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.)

UPDATE: This document is now outdated. Latest version is in the RFC.

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:

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:

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.

// 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*
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More โ†’
std::panic!("hello {}") - Error: Missing argument
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More โ†’
std::panic!("hello {}", 123) "hello 123" String
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More โ†’
std::panic!(concat!("he", "llo")) "hello" String or &'static str*
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More โ†’
std::panic!(STATIC_STR) Error: Use panic_any(STATIC_STR) instead**
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More โ†’
***
std::panic!(string.as_str()) - Error: Need 'static
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More โ†’
std::panic!(123) Error: Use panic_any(123) instead
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More โ†’
Core panic call fmt::Arguments in PanicInfo::message() Same as before
core::panic!("hello") format_args!("hello")
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More โ†’
core::panic!("hello {}") Error: Missing argument
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More โ†’
core::panic!("hello {}", 123) format_args("hello {}", 123)
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More โ†’
core::panic!(concat!("he", "llo")) format_args!("hello")
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More โ†’
core::panic!(STATIC_STR) Error: Need string literal, use panic!("{}", ..)
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More โ†’
***
core::panic!(string.as_str()) Error: Need 'static, use panic!("{}", ..) instead
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More โ†’
core::panic!(123) Error: Need &str
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More โ†’

*: 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, 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

    This comment also shows people do use this: comment on #51999.

  • panic!(some_expr)

    grep.app results

    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.