---
title: "4.10 Numeric conversions with as"
description: "Explicit casts in Rust: widening, truncation, float-to-int saturation, and why every conversion is your signature on a form."
url: "https://learnrust.net/chapter-4/numeric-conversions/"
last_updated: "2026-06-11"
---

# 4.10 Numeric conversions with as

This chapter has repeatedly slammed a door: no implicit conversions, not between `i32` and `u32`, not even from `i32` up to roomy, harmless `i64` (lesson [4.2](https://learnrust.net/chapter-4/integer-types/)'s quiz made sure you felt it). This lesson opens the door that was always meant to be used instead: explicit conversion, where *you* write the cast and thereby sign for its consequences.

If you're keeping score against C++: this single lesson replaces their entire implicit-conversion apparatus (promotions, narrowing rules, a chapter of "what converts to what when"), because a system with no automatic conversions has no automatic-conversion rules to memorize. There's one keyword, and it always means "I asked for this."

## The as keyword

```rust
fn main() {
    let small: i32 = 100;
    let big: i64 = small as i64;
    println!("{big}");
}
```

`value as Type` converts between numeric types (and a few friends, like the `char`-to-code casts of lesson [4.8](https://learnrust.net/chapter-4/char-and-unicode/)). Conversions *up* in size, like this one, are exact: every `i32` value fits in an `i64`, nothing can go wrong, and the cast is just paperwork the compiler demands so the reader can see the type change happening.

The interesting cases are the lossy directions, and `as` performs them *without complaint*, which is exactly why it must be written explicitly. Know the three behaviors:

**Narrowing integer casts truncate.** Converting to a smaller type keeps only the bits that fit:

```rust
fn main() {
    let n: i32 = 300;
    println!("{}", n as u8);
    println!("{}", (-1i32) as u8);
}
```

```
44
255
```

300 doesn't fit in a `u8`, so the excess bits are discarded, leaving 44 (300 − 256, the odometer arithmetic of lesson [4.4](https://learnrust.net/chapter-4/integer-overflow/)). And −1's bit pattern reinterpreted as unsigned is 255. Neither is an error, a panic, or a warning: `as` does what you asked, literally, and "what you asked" for a narrowing cast is "keep what fits."

> **Warning**
>
> That `i32` suffix on `(-1i32)` is load-bearing, and the reason is a lovely collision of two things you know. Casting a bare negative literal to an unsigned type (`-1 as u8`) fails to compile: the cast supplies the literal's type evidence (lesson [4.9](https://learnrust.net/chapter-4/type-inference-and-annotations/)'s rules at work), the `1` gets typed as `u8`, and *then* the minus sign has nothing legal to do, since unsigned values can't be negated (error E0600, whose help text drolly suggests you may have meant `u8::MAX`). Parentheses don't help; inference sees through them. A suffix pins the literal's type before the cast gets a vote. In real code this rarely bites, because real casts start from variables (like `n` above), which already have types.

**Float-to-int casts drop the fraction, then saturate.** The fractional part is discarded (toward zero, not rounded), and values beyond the target's range pin at its boundaries instead of wrapping:

```rust
fn main() {
    println!("{}", 3.9 as i32);
    println!("{}", (-3.9) as i32);
    println!("{}", 1e10 as i32);
    println!("{}", (-7.5) as u8);
    println!("{}", f64::NAN as i32);
}
```

```
3
-3
2147483647
0
0
```

3.9 becomes 3 (not 4; this is truncation, not rounding), ten billion pins at `i32::MAX`, negative values pin at a `u8`'s floor of 0, and NaN converts to 0 by decree. Defined, predictable, and occasionally surprising, which is the recurring theme.

**Int-to-float casts can lose precision.** Every `i32` survives the trip to `f64` exactly, but lesson [4.5](https://learnrust.net/chapter-4/floating-point-types/) told you an `f64` only carries 15-ish significant digits, and big 64-bit integers have more:

```rust
fn main() {
    let exact: i64 = 9_007_199_254_740_993;
    println!("{}", exact as f64);
}
```

```
9007199254740992
```

Off by one: the integer landed between representable floats and snapped to a neighbor. Rare in practice, memorable when met.

> **Warning**
>
> `as` never fails, and that's its sharp edge: it will quietly truncate, saturate, and approximate, because you signed for it. Before any *narrowing* cast, ask the lesson-3.6 defensive question: can this value actually be out of range here? If the answer is "maybe," a silent 44-from-300 is a bug in costume, and you want a conversion that can *say no* instead. Those exist (`try_from`, returning chapter 12's Result), and lesson 16.8 makes them routine. Until then: cast where you can argue it's safe, and leave a comment when the argument isn't obvious.

> **Key insight**
>
> The design pattern, third appearance this chapter (overflow policies, no truthiness, now casts): Rust doesn't forbid dangerous operations, it forbids *unmarked* ones. An implicit C++ conversion and a Rust `as` cast can compute the same wrong 44; the difference is that one is invisible in the source and the other is a keyword you typed, greppable in code review, with your name on it. Safety here isn't padding on the walls. It's a paper trail.

For completeness, the conversions this lesson is *not* about: numbers-from-text is `parse` (lesson 5.6), text-from-numbers is `format!` (5.5), and the ergonomic, can't-lose conversions between types get a nicer spelling (`From`/`Into`) in chapter 16. `as` is specifically the low-level numeric tool: blunt, total, explicit.

## Quiz time

**Question #1**

Predict each output:

```rust
fn main() {
    println!("{}", 9.99 as i32);
    let n = 256;
    println!("{}", n as u8);
    println!("{}", 255 as u8);
    println!("{}", (-1.0) as u32);
}
```

<details class="solution">
<summary>Show solution</summary>

```
9
0
255
0
```

Truncation toward zero (9, not 10); 256 is one past the `u8` odometer, wrapping the kept bits to 0; 255 fits exactly; −1.0 is below `u32`'s floor and saturates to 0 (float-to-int saturates; only *integer*-to-integer narrowing wraps bits — that contrast is quiz-worthy on purpose).

Bonus marks if you wondered why the 256 took the scenic route through a variable: written directly, `256 as u8` doesn't compile! The literal-out-of-range check from lesson [4.3](https://learnrust.net/chapter-4/isize-usize-and-integer-literals/) inspects literals even in cast position and refuses. The variable smuggles the value past the literal police and into honest runtime wrap territory, which is exactly the compile-time-versus-runtime boundary this chapter keeps tracing.

</details>

**Question #2**

This program reports the size of a 5 GB file. Predict its output, then diagnose.

```rust
fn file_size_in_bytes() -> u64 {
    5_000_000_000
}

fn main() {
    let size = file_size_in_bytes();
    let reported = size as u32;
    println!("file is {reported} bytes");
}
```

<details class="solution">
<summary>Show solution</summary>

```
file is 705032704 bytes
```

Five billion doesn't fit in a `u32` (ceiling: 4,294,967,295), so the cast keeps the bits that fit: 5,000,000,000 − 4,294,967,296 = 705,032,704. A wildly wrong size, no error anywhere, and every file over 4 GiB triggers it. The fixes: keep it `u64` end to end, or use a checked conversion that can report "doesn't fit" (lesson 16.8's `try_from`). The cast compiling cleanly is the point of the lesson: `as` made the risk visible in source; noticing it is the reviewer's job, currently yours.

</details>

**Question #3**

Why does Rust make you write `as i64` even for the always-safe `i32` → `i64` direction?

<details class="solution">
<summary>Show solution</summary>

Uniformity over cleverness: with *zero* implicit conversions, every type change in the program is visible in the source, so readers never wonder where a value silently changed shape, and there's no rulebook of "which conversions are automatic" to learn or misremember. (Whether *safe* conversions deserve nicer ergonomics is a real question with a real answer: `From`/`Into`, chapter 16.)

</details>

One lesson left before the chapter quiz, and it answers a question chapter 2 planted: functions return exactly one value... so how do you return two?

## Sitemap

See the full [sitemap](https://learnrust.net/sitemap.md) for all pages.
