Question

How to Convert a Slice to a Fixed-Size Array in Rust

rustarrays

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
Take Quiz

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

  1. data is created as:

    [10, 20, 30, 40, 50]
    
  2. pop(&data) passes a slice of the full array into pop.

    Inside pop, barry has type:

    &[u8]
    
  3. barry[0..3] creates a slice containing the first three values:

    &[10, 20, 30]
    
  4. .try_into() attempts to turn that slice into a fixed-size array:

    [u8; 3]
    
  5. Because the slice length is exactly 3, the conversion succeeds.

  6. unwrap() extracts the successful result.

  7. result becomes:

    [10, 20, 30]
    
  8. 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.

Take Quiz

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 barry has 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 Option or Result instead

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 bounds
  • get(0..3) returns None

Prefer get() when input may be malformed.

Comparisons

ApproachReturnsPanics on short input?Copies data?Best for
barry[0..3]&[u8]YesNoBorrowing a slice
[barry[0], barry[1], barry[2]][u8; 3]YesYesVery small manual arrays
barry[0..3].try_into().unwrap()[u8; 3]YesYesQuick conversions when length is guaranteed
barry.get(0..3)?.try_into().ok()Option<[u8; 3]>NoYesSafe parsing
barry.get(0..3)?.try_into().ok() as Option<&[u8; 3]>Option<&[u8; 3]>NoNoSafe borrowing without copy

Slice vs array

TypeExampleSize known at compile time?Owns data?
Array[u8; 3]YesYes
Slice reference&[u8]NoNo
Array reference&[u8; 3]YesNo
Take Quiz

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 Option or Result is usually safer than unwrap()

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.

Take Quiz

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
Take Quiz

Keep learning

Related questions

View more