DEV Community

Cover image for The Rust You Actually Need to Write Your First Anchor Program
Vincent Jande for 100 Days of Solana

Posted on

The Rust You Actually Need to Write Your First Anchor Program

If you have made it this far in 100 Days of Solana, you have been working in JavaScript and on the command line. You have been calling RPC methods, building instructions, signing transactions, and reading and writing account data in JavaScript, and most recently minting and sending tokens and NFTs from the CLI. Either way, you have been driving Solana with tools that let you assign a value and move on with your life. Soon the ground shifts. You are going to open a file called lib.rs, and it is going to be Rust, and for a day or two it is going to feel like you forgot how to program.

That feeling is normal, it is temporary, and it is not a sign you are in the wrong place. Here is the thing nobody says out loud: you do not need to learn all of Rust to write Solana programs. Rust is a big language with a steep reputation, but the slice of it that shows up in an Anchor program is small and repetitive. You will see the same handful of patterns on almost every line. Learn those patterns and the wall turns back into a floor.

This post is that handful. Not a Rust course, just the parts you need to read your first Anchor program and understand what every line is doing. Next week we start Arc 9, the Anchor introduction, where this all becomes real. This week is about making the language stop being scary before you get there.

Why it feels like a wall

JavaScript is dynamically typed and garbage collected. You write const x = 5, you never tell anyone it is a number, and when you are done with it the runtime quietly cleans up. The language trusts you and sorts out the consequences at runtime, which is why a typo surfaces as undefined is not a function three minutes into a demo.

Rust is the opposite philosophy. It is compiled and statically typed, so every value has a type the compiler knows about before the program ever runs, and it has no garbage collector, so it tracks who is responsible for every piece of memory through a system called ownership. The trade is blunt: Rust makes you front-load the thinking. The compiler refuses to build until the types line up and the ownership rules are satisfied. That is why the first day feels slow. You are paying upfront for something JavaScript billed you for later, usually in production.

The reassuring part, and this is real, is that the Rust compiler is the most helpful error reporter you have ever worked with. A JavaScript bug explodes at runtime with a vague message and no map. A Rust bug stops at compile time with an error that names the file, the line, the types involved, and very often the exact fix to type. You will spend the Anchor arc reading compiler errors. Get good at reading them slowly instead of panicking at the red text, because nine times out of ten the answer is sitting right there in the message.

The anatomy of the file you are about to meet

Before the individual pieces, here is the shape of a minimal Anchor program, taken straight from the Anchor program structure docs:

use anchor_lang::prelude::*;

declare_id!("11111111111111111111111111111111");

#[program]
mod hello_anchor {
    use super::*;
    pub fn initialize(ctx: Context<Initialize>, data: u64) -> Result<()> {
        ctx.accounts.new_account.data = data;
        msg!("Changed data to: {}!", data);
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(init, payer = signer, space = 8 + 8)]
    pub new_account: Account<'info, NewAccount>,
    #[account(mut)]
    pub signer: Signer<'info>,
    pub system_program: Program<'info, System>,
}

#[account]
pub struct NewAccount {
    data: u64,
}
Enter fullscreen mode Exit fullscreen mode

Every Anchor program you write next week is a variation on those four blocks. The rest of this post walks through the Rust you need to read them without flinching.

1. use and the import line

use anchor_lang::prelude::*;
Enter fullscreen mode Exit fullscreen mode

This is the Rust version of an import. use brings names into scope so you do not have to write the full path every time, and the * is a glob that pulls in everything from Anchor's prelude, a curated bundle of the types and macros nearly every program needs. It is the same instinct as importing from a package in JavaScript, just with a :: path separator instead of a dot and slashes. When you later see errors about a type "not found in this scope," a missing use line is the usual cause. You can read more in the Rust book chapter on packages and modules.

2. Macros: the lines with ! and #[...]

This is the single biggest "what am I looking at" moment for a JavaScript developer, so it gets the most space.

Rust has macros, which are code that writes code at compile time. They are not functions. You can spot them two ways. A ! after a name, like declare_id!(...), msg!(...), or require!(...), means a macro call. And the bracketed attributes sitting above things, like #[program], #[derive(Accounts)], and #[account], are also macros, the attribute kind, that transform the item directly below them.

Why this matters: Anchor is mostly macros. The whole point of the framework is that those attributes expand, at compile time, into the hundreds of lines of account-validation and serialization boilerplate that a raw Solana program makes you write by hand. The Anchor docs describe the four you will see constantly:

  • declare_id! sets your program's on-chain address.
  • #[program] marks the module that holds your instruction handlers. Each public function inside it becomes a callable instruction.
  • #[derive(Accounts)] goes on a struct to declare the list of accounts an instruction needs, and generates the validation for them.
  • #[account] goes on a struct to mark it as a custom account type your program stores data in. Among other things it stamps an 8-byte discriminator on the account so the program can later prove the account is really one of its own.

You do not need to know how to write a macro. You need to recognize that when you see #[derive(Accounts)], an enormous amount of code you did not write is being generated for you, and that this is the framework doing its job. Treat the attributes as configuration you are declaring, not logic you are reading. The Rust book's macro chapter is there if you get curious, but it is genuinely optional for the Anchor arc.

3. Structs: your data shapes

#[account]
pub struct NewAccount {
    data: u64,
}
Enter fullscreen mode Exit fullscreen mode

A struct is just a named bundle of fields, the same idea as a plain JavaScript object with a fixed shape, except the shape is declared and the compiler enforces it. NewAccount has one field, data, of type u64, an unsigned 64-bit integer. You will define a struct for every kind of account your program stores, and another struct for every instruction's account list (those are the #[derive(Accounts)] ones). When you read an Anchor program, structs are where you learn what data exists and what each instruction touches.

A note on number types, because JavaScript hides this from you. JavaScript has one number. Rust makes you pick: u64 is an unsigned (non-negative) 64-bit integer, i64 is signed, u8 is a byte, and so on. On Solana, u64 is everywhere because token amounts and counts are 64-bit. Picking the type is part of declaring your data, and it is why a counter's field is count: u64 and not just "a number."

4. pub: who can see this

pub means public. By default everything in Rust is private to its module, and pub opts an item into being visible outside it. In Anchor you will see pub fn on instruction handlers (the framework needs to call them from outside the module) and pub on struct fields you want readable. It is access control at the language level, closer to export in JavaScript modules than to anything in a plain object.

5. Functions and the return type

pub fn initialize(ctx: Context<Initialize>, data: u64) -> Result<()> {
    // ...
    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

fn declares a function. The parameters list their types after a colon, data: u64, the same "name then type" order you have already seen in the struct. The part that is new is -> Result<()>, the return type, which leads straight into the most important Rust concept for the week ahead.

6. Result, Ok, and the ? operator: error handling without try/catch

JavaScript handles errors by throwing and catching. Something fails, it throws, and somewhere up the stack a try/catch maybe deals with it, or maybe it crashes. Nothing in the type system forces you to handle it.

Rust has no exceptions. Instead, a function that can fail returns a Result, a type that is either Ok(value) for success or Err(error) for failure. Every Anchor instruction handler returns Result<()>, which reads as "this either succeeds with nothing meaningful to return, the empty (), or it fails with an error." That is why every successful handler ends with Ok(()): you are explicitly returning the success case. The Anchor docs note that all Anchor instruction handlers return this Result type.

The companion to Result is the ? operator, and it is worth learning early because it is everywhere. When you call something that returns a Result and write a ? after it:

let counter = something_that_can_fail()?;
Enter fullscreen mode Exit fullscreen mode

it means "if this is Ok, unwrap the value and keep going; if it is Err, stop this function right now and return that error to the caller." It is the same job a try/catch does, compressed into one character, and it is how Anchor programs bubble failures up to the runtime cleanly. When you see ?, read it as "this line might bail out early, and that is intentional."

You will also meet Option, the cousin of Result: it is Some(value) or None, and it is how Rust represents "this might be absent" instead of JavaScript's null or undefined. The reason Rust almost never has null-pointer surprises is that absence is a type you are forced to handle, not a landmine.

7. References and &mut: the ownership stuff

Here is the concept with no JavaScript equivalent, and the one that produces the most head-scratching. Rust tracks ownership of data. Every value has one owner, and when you want to let other code use a value without handing over ownership, you lend it out with a reference, written &. A plain & is a read-only borrow. &mut is a mutable borrow, meaning "I am lending you this and you are allowed to change it."

You will see this constantly in handlers:

let counter = &mut ctx.accounts.counter;
counter.count = 0;
Enter fullscreen mode Exit fullscreen mode

Read &mut ctx.accounts.counter as "get a mutable borrow of the counter account so I can write to it." Without &mut, you would have a read-only view and the compiler would reject the assignment. The rule the compiler enforces, and the source of most early borrow-checker errors, is that you can have many readers or one writer, but not both at once. That sounds restrictive until you realize it is the rule that makes whole categories of bugs impossible.

For the Anchor arc you do not need the deep theory. You need to recognize that & means "borrowed, look but do not take," &mut means "borrowed, allowed to change," and that when the compiler complains about borrowing it is protecting you from two pieces of code stepping on the same data. When you genuinely want to understand it, the ownership chapter of the Rust book is the clearest explanation written, and the Pubkey types being small and Copy (cheap to duplicate, so you often pass them by value) is a footnote you will appreciate later.

8. Custom errors with enums

When your program needs to reject something with a meaningful reason, you define an error type as an enum, a type that is one of a fixed set of named variants:

#[error_code]
pub enum CounterError {
    #[msg("The counter has reached its maximum value.")]
    Overflow,
}
Enter fullscreen mode Exit fullscreen mode

An enum in Rust is more powerful than a JavaScript constant list, but for now read it as "a closed set of named options." The #[error_code] attribute is Anchor wiring it into the program's error system, and the #[msg(...)] lines are the human-readable strings that show up in logs when that error fires. You then raise one with the require! macro or by returning Err(...). The Anchor errors guide covers the full pattern, and you will use it the moment your program needs to say no to bad input.

Putting it together

Read that minimal program one more time and notice you can now name every piece. use pulls in Anchor. declare_id! is a macro setting the address. #[program] is an attribute macro marking the handler module. initialize is a public function taking a Context and some data, returning a Result, ending in Ok(()). The #[derive(Accounts)] struct declares the accounts. The #[account] struct declares the stored data shape, with a u64 field. None of it is mysterious anymore. It is the same eight ideas, arranged.

That is the whole trick this week. You are not learning Rust the way a systems programmer learns Rust. You are learning to read a specific, repetitive dialect of it, and the vocabulary is small: imports, macros, structs, pub, functions, Result and ?, references, and enums. Every program in the arc ahead is built from those.

How to actually get through the week

A few habits that will save you real time.

Read the compiler errors top to bottom, slowly. Rust errors are long because they are thorough, not because they are angry. The first error is usually the real one; later errors are often just fallout. Fix the top one, recompile, repeat.

Compile early and often. Do not write thirty lines and then build. Write a few, run anchor build, see if it is happy, continue. Short feedback loops turn a scary language into a conversation.

When the borrow checker fights you, stop and ask who owns this and who is borrowing it, rather than randomly adding and removing & until it builds. The error message almost always tells you which rule you broke.

And keep one tab open on the Anchor program structure page and one on the Rust book. You will not read either cover to cover. You will look up exactly the thing in front of you, which is the right way to learn a language you are using rather than studying.

Next week the challenges move into Arc 9, the Anchor introduction, and these blog posts move with them. We leave the language topic behind here, not the program itself, and dig into Anchor properly: how accounts really work, what a Program Derived Address is and why it changes everything, and how the constraint system turns a struct into a security model. The daily challenges keep going as always; it is just this post's focus on the Rust language that wraps up today. For now, the goal is smaller and more important. Open lib.rs, recognize the pieces, and let the compiler teach you. You already did the hard part by getting here.

And if you have not joined the challenge yet, this is the moment. We are about to start writing real Solana programs, which is the part everything so far has been building toward. Jump in at mlh.link/solana-100 and build it with us.

Top comments (0)