Rust never 타입 안정화, 1.100에서 fallback이 ()에서 !로 바뀐다
Rust의 never 타입, 즉 !를 안정화하는 PR이 2026년 8월 24일 병합됐습니다. 먼저 짚을 것은 아직 stable이 아니라는 점입니다. 오늘(8월 27일) 기준 stable은 8월 20일에 나온 1.98.0이고 이 변경은 1.100.0에 실립니다. 1.100.0은 master 브랜치 컷이 9월 25일, stable 출시가 11월 12일 예정이라 77일이 남았습니다. 중간의 1.99.0(10월 1일)에는 이 변경이 없습니다. 구현자 본인도 현재 상태를 stable (on nightly)이라고 적었습니다.
PR 제목은 "stabilize never type" 하나지만 하는 일은 네 가지이고, 그중 never type fallback을 전 에디션에서 ()에서 !로 바꾸는 항목은 PR 본문이 스스로 breaking change라고 적었습니다. 내 코드가 안 될 수 있는 지점은 여기입니다.
순서는 오해하기 쉽습니다. 에디션 2024를 쓰고 있다면 같은 변경을 2025년 2월 1.85.0부터 이미 겪었습니다. 에디션 2021 이하에서는 이 fallback에 의존하는 코드가 2025년 12월 1.92.0부터 컴파일 오류였습니다. 다만 오류는 직접 빌드하는 크레이트에서만 나고 의존성에서 나온 것은 cargo 경고에 그쳤습니다. 그래서 지금 오류가 없다는 것이 안전을 뜻하지는 않습니다 — 크레이터 실패 3천 건 가까이가 옛 버전 의존성에서 나왔습니다.
| 에디션 | 지금 | 1.100.0부터 |
|---|---|---|
| 2021 이하 | fallback () · 이에 의존하는 코드는 lint 오류(deny) · #[allow] 억제 가능 |
fallback ! · lint 제거로 그냥 타입 오류 |
| 2024 | fallback ! · 1.85.0부터 타입 오류 |
같음 |
10년 사이 병합된 뒤 되돌려진 것이 두 번 있었다.
PR 하나가 네 가지를 바꿉니다
병합된 것은 rust-lang/rust의 PR 155499(작성자 WaffleLapkin)입니다. 4월 18일에 열려 8월 24일 22시 30분(UTC)에 병합됐고 마일스톤은 1.100.0입니다.
PR 본문에 적힌 네 항목은 never 타입(!) 안정화, never type fallback을 전 에디션에서 ()에서 !로, 표준 라이브러리 Infallible을 !의 별칭으로, 그리고 dependency_on_unit_never_type_fallback lint 제거입니다. 마지막 항목의 이유는 ()로 떨어지는 fallback이 없어지므로 이 lint가 발동할 수 없다는 것입니다. 이 병합으로 트래킹 이슈 35121·148922가 닫혔습니다.
오해를 하나 정리하면, fn f() -> !처럼 반환 위치에 !를 쓰는 것은 예전부터 되던 일입니다. 이번에 안정화된 것은 !를 제네릭 인자나 연관 타입, 필드 타입처럼 타입이 놓이는 자리 전반에서 쓰는 것입니다.
fn foo() -> !;
fn call_a_fn<T, F: Fn() -> T>(f: F) -> T;
call_a_fn(foo) // ERROR!10년, 그리고 되돌려진 두 번
RFC 1216 "Promote ! to a type."는 시작 날짜가 2015년 7월 19일, 병합이 2016년 7월 29일입니다. 기능 이름은 bang_type이고 RFC 헤더에 적힌 Rust 이슈가 위에서 말한 35121입니다.
그 사이 병합된 뒤 되돌려진 것이 두 번 있습니다. 2018년 3월 15일 병합된 PR 47630은 한 달 뒤 PR 50121로(마일스톤 1.26, 사유 이슈 49691), 2019년 11월 21일 병합돼 1.41.0을 겨냥했던 PR 65355는 12월 15일 PR 67224로(사유 이슈 66757) 되돌려졌습니다. 두 번 다 fallback 의미론을 원래대로 돌려놓았습니다.
구현자는 블로그에 10년간 불안정했고 다섯 번의 안정화 시도가 실패했으며 자신이 2년 넘게 이 일을 해 왔다고 적었습니다. 다섯 번이라는 수는 구현자 본인의 진술이고 저장소에서 확인한 것은 위의 되돌림 두 건입니다. 되돌림 이력이 있고 1.100.0까지 beta 단계가 남아 있으므로, 이 글에서도 stable 출시는 예정으로만 적습니다.
핵심은 fallback이 ()에서 !로 바뀌는 것입니다
never-to-any 강제변환에서 나온 추론 변수를 컴파일러가 추론할 수 없을 때가 있습니다. never type fallback은 그때 대신 채워 넣는 기본 타입입니다. 역사적으로 그 값이 ()였고 이번에 !로 바뀝니다. 아래 예시는 에디션 가이드와 표준 라이브러리 문서에 실린 것입니다.
// this
let x: u8 = panic!();
// is (essentially) turned by the compiler into
let x: u8 = absurd(panic!());
// where absurd is a function with the following signature
// (it's sound, because `!` always marks unreachable code):
fn absurd<T>(_: !) -> T { ... }바꾸는 이유는 구현자가 PR 코멘트에 적었습니다. () 기본 처리가 "특히 클로저 주변에서 ! 타입 값이 저절로 붕괴하는" 혼란스러운 동작을 낳는다는 것입니다. 바깥에서 관측되는 차이는 에디션 2024 트래킹 이슈의 예시가 짧습니다.
print_return_type(|| todo!());
// before this change: prints `()`
// after this change: prints `!`같은 코드가 fallback 값에 따라 컴파일되기도, 실패하기도 한다.
이미 2025년 12월부터 오류였습니다
dependency_on_unit_never_type_fallback은 () fallback에 의존하는 코드를 잡아내는 future-compat lint입니다. 구현 PR은 125289인데 어느 릴리스에 처음 들어갔는지는 1차 출처로 확인하지 못했으므로 버전은 적지 않겠습니다. 1.89.0(2025년 8월 7일)에는 의존성 쪽 코드까지 보고하도록 넓어졌고(PR 141937), 1.92.0(2025년 12월 11일)에서 deny-by-default로 바뀌었습니다(PR 146167).
1.92.0 발표문은 이 lint와 never_type_fallback_flowing_into_unsafe가 deny-by-default가 되어 "검출되면 컴파일 오류를 일으킨다"고 적었습니다. 영향 크레이트는 약 500개로 집계됐고, 앞서 본 두 조건(#[allow]로 억제 가능, 의존성에서 나온 것은 cargo 경고)도 같은 문서에 적혀 있습니다.
1.100.0에서 달라지는 지점이 여기입니다. lint가 제거되므로 같은 코드는 lint 오류가 아니라 그냥 타입 오류가 됩니다. #[allow]로 눌러 둔 곳이 있다면 그 억제는 더 이상 효과가 없습니다. 한편 never_type_fallback_flowing_into_unsafe는 이번에 제거되지 않습니다. 왜 이쪽을 남기는지는 원문에서 확인하지 못했습니다.
1.100.0에서는 lint 자체가 없어져 allow 로 억제할 수 없다.
내 코드에서 깨지는 지점
? 연산자가 가장 흔합니다
구현자의 크레이터 분석에 따르면 깨짐의 대다수는 ? 연산자에서 나옵니다. 타입이 정해지지 않은 채 ?를 쓰면 추론 변수가 fallback으로 채워지는데, ()에는 있던 Default 구현이 !에는 없습니다.
fn x<T: Default>() -> Result<T, ()> { Ok(T::default()) }
// Before fallback change: `?` makes rustc infer T=(); (): Default; OK
// After fallback change: `?` makes rustc infer T=!; !: !Default; ERR
x()?;고침은 타입을 직접 적는 것입니다. x::<()>()?; 또는 () = x()?; 입니다. 직접 깨진 7건 중 4건은 redis 크레이트를 값 타입 지정 없이 쓴 경우였습니다.
클로저와 분기, 그리고 unsafe
에디션 가이드의 마이그레이션 절에는 클로저 안에서 panic!을 쓰는 예시가 있습니다. 반환 타입을 -> ()로 적어 주면 됩니다.
trait Unit {}
impl Unit for () {}
fn run<R: Unit>(f: impl FnOnce() -> R) {
f();
}
run(|| panic!()); // 깨짐
run(|| -> () { panic!() }); // 고침분기 한쪽이 !인 경우도 같아서, 아래 코드는 fallback이 ()면 컴파일되고 !면 실패합니다. 좌변에 () =를 붙여 타입을 고정하면 됩니다.
if true {
return
} else {
Default::default()
};같은 모양이 unsafe와 만나면 훨씬 나쁩니다. 아래 코드는 fallback이 ()면 건전하지만 !면 즉시 정의되지 않은 동작이 됩니다. std::mem::zeroed::<()>()처럼 타입을 박아 두면 되고 이 상황을 잡는 lint는 이번에도 남습니다.
if true {
return
} else {
unsafe { std::mem::zeroed() }
};크레이터가 말한 것과 말하지 않은 것
4월 20일 craterbot 코멘트는 전체 9,024건 중 3,277건 회귀, 0건 수정으로 보고했습니다. 같은 코멘트의 "1149 spurious results"는 그중 1,149건이 허위라는 뜻이 아니라 재시도 후보 목록의 규모입니다.
그래서 이 수치를 "3,277개 크레이트가 깨진다"로 읽으면 안 됩니다. 앞서 본 대로 대부분은 이미 패치된 라이브러리의 옛 버전에 의존한 결과입니다. 다음 날 구현자가 올린 분석에서 Primary breakage 절이 꼽은 직접 깨진 크레이트는 7건입니다 — rups, helloworld-tonic, srt-tokio, redis-lock, rust-trending, alion-admin-api, cache-any입니다.
같은 코멘트의 "significant" issues 절은 층이 다릅니다 — 역의존이 많아 따로 확인한 목록입니다. redis와 sqlx-postgres 0.7.4(역의존 618개), solana-client, wl-clipboard-rs, mlua, trie-db, ashpd가 여기 올랐고 전부 상위 버전에 수정판이 있습니다. 수정판이 없는 것은 두 곳뿐이고 둘 다 후속 크레이트로 대체됐습니다(rlua→mlua, screenshots→xcap).
회귀 3,277건과 직접 깨짐 7건은 층이 다른 숫자다.
Infallible 이 타입 별칭이 됩니다
표준 라이브러리의 Infallible은 pub enum Infallible에서 pub type Infallible = !;로 바뀝니다. 안정화 표기는 1.34.0으로 남지만 문서 주소가 type.Infallible.html로 옮겨가 예전 링크는 끊어집니다. 이를 위해 From<!> for T reservation impl이 먼저 제거됐습니다 — 안정화 PR보다 앞서 병합된 PR 160705입니다.
cargo-semver-checks는 이 변경을 major breaking으로 잡지만 논의에서 의도된 결과로 정리됐습니다. 승인 이슈는 별도로 155924이고, 8월 27일 현재 FCP가 끝난 disposition-merge 상태로 아직 열려 있습니다. 그 이슈는 깨짐을 크레이트 수로 집계했습니다. 강제변환된 never 타입에 메서드를 호출하는 경우(anyhow::anyhow!(e)) 약 20개, 안정성 검사 우회 9개, reservation impl 충돌 14개, Box<dyn Send> 강제변환 1개입니다. 이 집계는 PR 155500·155501에서 따로 돌린 크레이터 실험 결과로, 앞의 155499 리포트(9,024/3,277/7)와는 다른 실험입니다. 가장 큰 건은 anyhow 호환이라고 본인이 적었습니다.
stable은 11월 12일 — 지금 확인하는 법
지금 확인할 경로는 nightly입니다. beta 채널이 언제 1.100.0으로 넘어가는지는 릴리스 일정에 적혀 있지 않습니다. 확인은 #[allow(dependency_on_unit_never_type_fallback)]이 걸린 자리부터가 빠릅니다. 1.92.0부터 deny였던 lint를 눌러 둔 자리가 곧 타입 오류가 됩니다. 그다음은 위에서 본 ? 연산자와 클로저 패턴, 옛 버전에 머문 의존성입니다.
nightly 표준 라이브러리 문서에는 never 타입의 안정화 버전이 1.100.0으로 이미 표기돼 있습니다. 다만 같은 페이지의 fallback 설명 절은 낡아 있어 그 문장만 읽으면 변경이 아직 남은 일처럼 읽힙니다. 재작성 PR 158370이 8월 26일 병합됐습니다.
근거와 참고
- PR #155499 never 타입 안정화
- #155499 코멘트 — fallback 변경 이유 · craterbot 결과 · 구현자 분석
- RFC 1216 Promote
!to a type - 이슈 #148922 fallback 변경 트래킹 · #123748 에디션 2024 트래킹
- 이슈 #155924 Make
Infallible = ! - 되돌림 PR #50121 · #67224
- PR #146167 lint deny-by-default
- PR #160705 reservation impl 제거
- releases.rs 1.100.0 일정
- Rust 1.85.0 발표문
- Rust 1.92.0 발표문
- 에디션 2024 가이드 — fallback
- nightly std — never 타입 · Infallible
- 참고 — 구현자 블로그