#059 Apr 3, 2026

59. split_first_chunk — Destructure Slices into Arrays

Parsing a header from a byte buffer? Extracting the first N elements of a slice? split_first_chunk hands you a fixed-size array and the remainder in one call — no manual indexing, no panics.

The Problem

You have a byte slice and need to pull out a fixed-size prefix — say a 4-byte magic number or a 2-byte length field. The manual approach is fragile:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
fn parse_header(data: &[u8]) -> Option<([u8; 4], &[u8])> {
    if data.len() < 4 {
        return None;
    }
    let header: [u8; 4] = data[..4].try_into().unwrap();
    let rest = &data[4..];
    Some((header, rest))
}

fn main() {
    let packet = b"RUST is awesome";
    let (header, rest) = parse_header(packet).unwrap();
    assert_eq!(&header, b"RUST");
    assert_eq!(rest, b" is awesome");
}

That try_into().unwrap() is ugly, and if you get the index arithmetic wrong, you get a panic at runtime.

After: split_first_chunk

Stabilized in Rust 1.77, split_first_chunk splits a slice into a &[T; N] array reference and the remaining slice — returning None if the slice is too short:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
fn parse_header(data: &[u8]) -> Option<(&[u8; 4], &[u8])> {
    data.split_first_chunk::<4>()
}

fn main() {
    let packet = b"RUST is awesome";
    let (magic, rest) = parse_header(packet).unwrap();
    assert_eq!(magic, b"RUST");
    assert_eq!(rest, b" is awesome");

    // Too short — returns None instead of panicking
    let tiny = b"RS";
    assert!(tiny.split_first_chunk::<4>().is_none());
}

One method call. No manual slicing, no try_into, and the const generic N ensures the compiler knows the exact array size.

Chaining Chunks for Protocol Parsing

Real protocols have multiple fields. Chain split_first_chunk calls to peel them off one at a time:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
fn parse_packet(data: &[u8]) -> Option<([u8; 2], [u8; 4], &[u8])> {
    let (version, rest) = data.split_first_chunk::<2>()?;
    let (length, payload) = rest.split_first_chunk::<4>()?;
    Some((*version, *length, payload))
}

fn main() {
    let raw = b"\x01\x02\x00\x00\x00\x05hello";
    let (version, length, payload) = parse_packet(raw).unwrap();

    assert_eq!(version, [0x01, 0x02]);
    assert_eq!(length, [0x00, 0x00, 0x00, 0x05]);
    assert_eq!(payload, b"hello");
}

Each ? short-circuits if the remaining data is too short. No bounds checks scattered across your code.

From the Other End: split_last_chunk

Need to grab a suffix instead — like a trailing checksum? split_last_chunk mirrors the API from the back:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
fn strip_checksum(data: &[u8]) -> Option<(&[u8], &[u8; 2])> {
    data.split_last_chunk::<2>()
}

fn main() {
    let msg = b"payload\xAB\xCD";
    let (body, checksum) = strip_checksum(msg).unwrap();
    assert_eq!(body, b"payload");
    assert_eq!(checksum, &[0xAB, 0xCD]);

    let short = b"\x01";
    assert!(strip_checksum(short).is_none());
}

Same safety, same ergonomics — just peeling from the tail.

The Full Family

These methods come in mutable variants too, all stabilized in 1.77:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
fn main() {
    // Immutable — borrow array refs from a slice
    let data: &[u8] = &[1, 2, 3, 4, 5];
    let (head, tail) = data.split_first_chunk::<2>().unwrap();
    assert_eq!(head, &[1, 2]);
    assert_eq!(tail, &[3, 4, 5]);

    // split_last_chunk — from the back
    let (init, last) = data.split_last_chunk::<2>().unwrap();
    assert_eq!(init, &[1, 2, 3]);
    assert_eq!(last, &[4, 5]);

    // first_chunk / last_chunk — just the array, no remainder
    let first: &[u8; 3] = data.first_chunk::<3>().unwrap();
    assert_eq!(first, &[1, 2, 3]);

    let last: &[u8; 3] = data.last_chunk::<3>().unwrap();
    assert_eq!(last, &[3, 4, 5]);
}

Wherever you reach for &data[..N] and a try_into(), there’s probably a chunk method that does it better. Type-safe, bounds-checked, and zero-cost.

← Previous 58. Option::is_none_or — The Guard Clause You Were Missing