Are lifetimes expected to be checked in unreachable code?

I was just refactoring code when I encountered the following. Consider this function:

fn foo() {
    let local_ref = &5;
    let capturing_ref = || {


fn assert_static<T: 'static>(val: &T) {}

As expected, it fails to compile as the closure references local_ref which doesn't have a 'static lifetime. However, when adding an innocuous todo!() (or I assume any other diverging expression) at the beginning of the function, it suddenly compiles.
I can't really imagine that this is intended behaviour, especially as this "trick" doesn't work in the following case:

// The following function fails to compile even with the todo!
fn take_first<'a, 'b>(x: &'a i32, y: &'b i32) -> &'a i32 {

Playground for both examples

Am I maybe wrong and this is expected? Or is it a known bug? I tried looking through the issues, but found nothing.

This function definition:

fn take_first<'a, 'b>(x: &'a i32, y: &'b i32) -> &'a i32

says the result 'a is borrowed from the same place as input 'a (the x), and that it can't be from 'b, because 'a and 'b are not related.

So when you try to return y that is borrowed from some location denoted by 'b, but return it from function saying it's definitely not from 'b, but from 'a, that's an error.

You can make it:

fn take_first<'a>(x: &'a i32, y: &'a i32) -> &'a i32

to say all of the args borrow from the same place, so either of them is compatible with the return type.


fn take_first<'a, 'b: 'a>(x: &'a i32, y: &'b i32) -> &'a i32

to say 'b lives as long or longer than 'a, therefore things borrowed from 'b are just as good as borrowed from 'a.

I would agree with the observation that lifetime checks for/with unreachable code are feeling somewhat inconsistent. If I had to guess, the difference between the examples might be that the latter is more of a type-mismatch error rather than a borrow-checking error; and type-checking generally happens even in unreachable code.

For example, modifying the take_first example to

// The following function fails to compile even with the todo!
fn take_first<'a, 'b>(x: &'a i32, y: &'b i32) -> &'a i32 {
    let n = 42; 

makes it compile, too; if you remove the todo!s, the error is E0515 instead of E0623.

It’s probably not really a bug; after all, considering control-flow is essential for borrow checking, and also what happens in unreachable code is fairly irrelevant anyways. On the other hand, the interaction is unfortunate/undesirable in many situations, considering that todo!() is intended to be removed eventually, so you might prefer getting the error earlier. It’s not something we can easily change though, either, because that would be a breaking change.


Thanks for your reply! I guess it makes sense that borrow checking depends on control flow and therefore is not performed for unreachable code. The interaction with todo!() is still unfortunate though.

My actual code causing the initial confusion was not as simple as in the OP. In my code, I had a let mut output = Mutex::new(todo!("Output size?"); in the beginning of a complex function because I didn't yet know the expected size of the output matrix that I want to allocate. I've often used the property that ! coerces to any type, but I guess from now on I'll be more careful. I'd rather have the lifetime errors earlier than later.

As a mitigation, you could e.g. call a function like

pub fn todo<T>(message: &str) -> T {
    todo!("{}", message)

instead; this way, the ! type and hence the unreachability of subsequent code doesn’t become visible to the compiler at the call-site.

1 Like

The other thing you can do is leverage the fact that it's only type-level unreachability that matters, not value-based unreachability.

So if true { todo!() } will probably also give you the "you have to have an implementation that compiles after it" behaviour, if you want.

1 Like

I'm not sure what you are getting at – ! does coerce to any type, and it doesn't make borrowck or type checking unsound. You don't particularly need to exercise extra care when working with !, it fits pretty well into the type system.

That's a good idea. I might use that in the future.

@H2CO3 I'm not implying that ! doesn't coerce or introduce unsoundness. What I was referring to was that unreachable code, e.g. through the usage of a diverging expression at the start of a function, is not (fully?) borrow checked as shown in the first example. This is annoying as it can hide lifetime problems, which may necessitate other data structures, smart pointers or restructuring the code in question. In my concrete case, I was writing a big closure which is passed to rayon::spawn and therefore needs to be static. I didn't notice until quite some time that what I was doing shouldn't be possible, as my closure was definitely not static. When investigating why my code compiled although it should not, I tracked it down to that todo!() call inside the Mutex::new call.
Like I said:

I'd rather have the lifetime errors earlier than later.

Ah, I see.

This seems like a strange way to put it. The borrow checker doesn't hit todo! and give up; it's still doing borrow checking, it's just borrow checking code that is partially unreachable. And, in this case, correctly determines that its unreachability makes it sound. It's unfortunate that isn't really helpful to you, but it's not as if borrow checking wasn't performed at all.

1 Like

Interestingly it does compile on nightly. If I'd had to guess, I would say this is because #![feature(nll)] was stabilized, removing exactly this kind of type-checking.


Interesting. And without the todo!() it produces an error called "lifetime may not live long enough" instead of the previous "lifetime mismatch" error. Notably that new error does not have an error code (IMHO it should probably get one though).

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.