Question
I have a byte slice in Rust, and I want to take part of it and return that data as a fixed-size array.
For example:
fn pop(barry: &[u8]) -> [u8; 3] {
barry[0..3]
}
This does not compile because barry[0..3] produces a slice, not an array. Rust reports a type mismatch similar to:
expected array `[u8; 3]`, found slice `[u8]`
How can I convert a slice into a statically sized array like [u8; 3]?
Short Answer
By the end of this page, you will understand the difference between Rust slices and fixed-size arrays, why a slice cannot automatically become an array, and the main ways to convert or copy slice data into [u8; 3] safely and clearly.
Concept
In Rust, arrays and slices are related but different types.
- An array like
[u8; 3]has a size known at compile time. - A slice like
&[u8]is a dynamically sized view into some sequence of elements.
That is why this code fails:
fn pop(barry: &[u8]) -> [u8; 3] {
barry[0..3]
}
barry[0..3] is a slice expression, so its type is &[u8], not [u8; 3].
Rust keeps these types separate because they have different guarantees:
[u8; 3]always contains exactly 3 bytes&[u8]may contain any number of bytes
If you want a fixed-size array, Rust needs proof that exactly 3 elements are present. In practice, that usually means one of these approaches:
- Copy from the slice into a new array
- Try converting the slice into an array reference or array value
- Use pattern matching when the size is known structurally
This matters in real programming because many APIs use slices for flexibility, while lower-level logic, binary formats, and protocol parsing often need fixed-size arrays.
Mental Model
Think of a slice as a sticky note pointing at part of a row of boxes.
- The sticky note says: "start here, and there are some items"
- But it does not permanently own exactly 3 boxes
A fixed-size array is like a small tray with exactly 3 slots.
- The tray always has room for exactly 3 items
- Rust treats that as a different kind of object
So when you write barry[0..3], you are getting a sticky note pointing at 3 items, not a new 3-slot tray. If you want the tray, you must either:
- build it by copying those 3 items, or
- convert the slice only if Rust can verify the length is exactly 3
Syntax and Examples
The most common beginner-friendly solution is to copy the slice into an array.
fn pop(barry: &[u8]) -> [u8; 3] {
[barry[0], barry[1], barry[2]]
}
This works because you are explicitly building a new array.
A more scalable approach is to use try_into():
use std::convert::TryInto;
fn pop(barry: &[u8]) -> [u8; 3] {
barry[0..3].try_into().unwrap()
}
What this does
barry[0..3]creates a slice&[u8]try_into()attempts to convert that slice into[u8; 3]unwrap()assumes the conversion succeeds
This is valid because the slice length is exactly 3.
If you want to avoid panicking, return a Result instead:
use std::convert::TryInto;
fn pop(barry: &[u8]) -> Result<[u8; 3], &'static str> {
if barry.len() < 3 {
return Err("slice must contain at least 3 elements");
}
barry[0..3].try_into().map_err(|_| "conversion failed")
}
You can also borrow a fixed-size array instead of creating a new owned one:
use std::convert::TryInto;
fn first_three(barry: &[u8]) -> &[u8; 3] {
barry[0..3].try_into().unwrap()
}
This returns a reference to an array, not a copied array value.
Step by Step Execution
Consider this example:
use std::convert::TryInto;
fn pop(barry: &[u8]) -> [u8; 3] {
barry[0..3].try_into().unwrap()
}
fn main() {
let data = [10, 20, 30, 40, 50];
let result = pop(&data);
println!("{:?}", result);
}
Step by step
-
datais created as:[10, 20, 30, 40, 50] -
pop(&data)passes a slice of the full array intopop.Inside
pop,barryhas type:&[u8] -
barry[0..3]creates a slice containing the first three values:&[10, 20, 30] -
.try_into()attempts to turn that slice into a fixed-size array:[u8; 3] -
Because the slice length is exactly 3, the conversion succeeds.
-
unwrap()extracts the successful result. -
resultbecomes:[10, 20, 30] -
The program prints:
[10, 20, 30]
Important note
If barry had fewer than 3 elements, barry[0..3] would panic before conversion even happens. That is why length checks are often important.
Real World Use Cases
Converting slices to fixed-size arrays is useful when working with structured binary data.
Common examples
- Network protocols: read exactly 4 bytes for an IPv4 address segment or header field
- File parsing: extract a fixed-size magic number from a file header
- Cryptography: convert chunks of bytes into fixed-size blocks or keys
- Embedded systems: read sensor packets with known byte lengths
- Image/audio formats: parse headers with exact byte counts
Example: reading a 3-byte RGB value from a byte buffer:
use std::convert::TryInto;
fn read_rgb(bytes: &[u8]) -> Option<[u8; 3]> {
bytes.get(0..3)?.try_into().ok()
}
This is especially common when external data arrives as slices, but your logic needs exact field sizes.
Real Codebase Usage
In real Rust codebases, developers usually prefer patterns that are both safe and expressive.
1. Guard clauses before slicing
Instead of assuming enough data exists, check first:
use std::convert::TryInto;
fn pop(barry: &[u8]) -> Option<[u8; 3]> {
if barry.len() < 3 {
return None;
}
barry[0..3].try_into().ok()
}
This avoids panics.
2. Using get() instead of direct indexing
use std::convert::TryInto;
fn pop(barry: &[u8]) -> Option<[u8; 3]> {
barry.get(0..3)?.try_into().ok()
}
This is a common production pattern because get() returns None instead of panicking.
3. Borrowing instead of copying when possible
If you only need to read the data, borrowing can be cheaper:
use std::convert::TryInto;
fn pop_ref(barry: &[u8]) -> Option<&[u8; 3]> {
barry.get(0..3)?.try_into().ok()
}
4. Parsing binary structures
Developers often chain these conversions while reading fields:
use std::convert::TryInto;
fn parse_header(bytes: &[u8]) -> Option<([u8; 3], u8)> {
let prefix: [u8; 3] = bytes.get(0..3)?.try_into().ok()?;
let version = *bytes.get(3)?;
Some((prefix, version))
}
This style appears often in protocol parsers and data decoders.
Common Mistakes
1. Expecting a slice expression to produce an array
Broken code:
fn pop(barry: &[u8]) -> [u8; 3] {
barry[0..3]
}
Why it fails:
barry[0..3]is a slice, not an array
Fix:
use std::convert::TryInto;
fn pop(barry: &[u8]) -> [u8; 3] {
barry[0..3].try_into().unwrap()
}
2. Indexing without checking length
Broken code:
fn pop(barry: &[u8]) -> [u8; 3] {
[barry[0], barry[1], barry[2]]
}
Why it is risky:
- It panics if
barryhas fewer than 3 elements
Safer version:
use std::convert::TryInto;
fn pop(barry: &[u8]) -> Option<[u8; 3]> {
barry.get(0..3)?.try_into().ok()
}
3. Using unwrap() in code that should handle bad input
use std::convert::TryInto;
fn pop(barry: &[u8]) -> [u8; 3] {
barry.get(0..3).unwrap().try_into().unwrap()
}
Why it is often a bad idea:
- invalid input causes a panic
- production code usually returns
OptionorResultinstead
4. Confusing &[u8; 3] with [u8; 3]
These are different:
[u8; 3]= owned array value&[u8; 3]= borrowed reference to an array
Example:
use std::convert::TryInto;
fn borrow_three(barry: &[u8]) -> Option<&[u8; 3]> {
barry.get(0..3)?.try_into().ok()
}
If you need ownership, copy it:
use std::convert::TryInto;
fn own_three(barry: &[u8]) -> Option<[u8; 3]> {
Some(barry.get(0..3)?.try_into().ok()?)
}
5. Forgetting that get(0..3) is often safer than [0..3]
[0..3]panics on invalid boundsget(0..3)returnsNone
Prefer get() when input may be malformed.
Comparisons
| Approach | Returns | Panics on short input? | Copies data? | Best for |
|---|---|---|---|---|
barry[0..3] | &[u8] | Yes | No | Borrowing a slice |
[barry[0], barry[1], barry[2]] | [u8; 3] | Yes | Yes | Very small manual arrays |
barry[0..3].try_into().unwrap() | [u8; 3] | Yes | Yes | Quick conversions when length is guaranteed |
barry.get(0..3)?.try_into().ok() | Option<[u8; 3]> | No | Yes | Safe parsing |
barry.get(0..3)?.try_into().ok() as Option<&[u8; 3]> | Option<&[u8; 3]> | No | No | Safe borrowing without copy |
Slice vs array
| Type | Example | Size known at compile time? | Owns data? |
|---|---|---|---|
| Array | [u8; 3] | Yes | Yes |
| Slice reference | &[u8] | No | No |
| Array reference | &[u8; 3] | Yes | No |
Cheat Sheet
// Slice from array
let s: &[u8] = &data[0..3];
// Copy slice into fixed-size array
use std::convert::TryInto;
let a: [u8; 3] = data[0..3].try_into().unwrap();
// Safe version
let a: Option<[u8; 3]> = data.get(0..3)?.try_into().ok();
// Borrow fixed-size array instead of copying
let a_ref: Option<&[u8; 3]> = data.get(0..3)?.try_into().ok();
Rules to remember
arr[a..b]gives a slice, not an array[T; N]and&[T]are different types- Use
try_into()to convert a slice to a fixed-size array - Use
get()instead of indexing when input may be too short - Returning
OptionorResultis usually safer thanunwrap()
Common safe pattern
use std::convert::TryInto;
fn first_three(bytes: &[u8]) -> Option<[u8; 3]> {
bytes.get(0..3)?.try_into().ok()
}
FAQ
Why does barry[0..3] return a slice instead of an array?
Because Rust slice syntax creates a view into existing data. It does not create a new fixed-size array value.
How do I safely convert a slice to [u8; 3] in Rust?
Use:
use std::convert::TryInto;
bytes.get(0..3)?.try_into().ok()
This avoids panics and returns Option<[u8; 3]>.
Should I return [u8; 3] or &[u8; 3]?
Return [u8; 3] if you need ownership of the data. Return &[u8; 3] if borrowing is enough and you want to avoid copying.
Does converting a slice to an array copy the data?
Converting to [u8; 3] copies the 3 elements. Converting to &[u8; 3] borrows without copying.
Is unwrap() okay here?
Only if you know the input is always valid. For user input, file data, or network data, prefer Option or Result.
Can I convert any slice to any array size?
Only if the slice length exactly matches the target array length.
What is the easiest way to build a small fixed-size array?
For tiny cases, manual construction is simple:
[barry[0], barry[1], barry[2]]
But it can panic if the slice is too short.
Mini Project
Description
Build a small Rust helper for parsing a packet prefix from raw bytes. Many binary formats begin with a fixed number of bytes that identify the message type or version. This project demonstrates how to safely extract a fixed-size array from a slice without panicking.
Goal
Create a function that reads the first 3 bytes from a byte slice and returns them safely as a [u8; 3].
Requirements
- Write a function that accepts
&[u8] - Return
Option<[u8; 3]> - Safely handle input shorter than 3 bytes
- Print the parsed prefix for valid input
- Print an error message for invalid input
Keep learning
Related questions
Absolute Values in Rust: Using the abs() Method
Learn how to calculate absolute values in modern Rust with abs(), handle signed integer overflow, and choose safer alternatives.
Accepting Iterators in Rust Functions with impl Iterator
Learn how to write Rust functions that accept HashMap iterators using generic bounds or impl Iterator, avoiding trait object and lifetime errors.
Access Enum Variant Values in Rust with Pattern Matching
Learn how to extract and print values stored in Rust enum variants using match and if let pattern matching, with practical examples.