Skip to content
>_ StartupJobList_

Interview questions · Worked answers

Rust interview questions and worked answers

Explain which value owns each resource, how long each borrow is valid, and which failures are represented in the type. Strong answers also separate compile-time guarantees from runtime coordination and performance evidence.

For: Rust developers writing services, libraries, and systems tools

Guide scope and versions

Covers stable Rust language and standard-library concepts. Async examples distinguish the language's Future model from the executor supplied by a runtime crate.

Question 1 of 9

What changes when a `String` moves into a function?

Passing a String by value moves ownership to the callee, so the caller cannot use that binding afterward. Copy types such as integers duplicate their bits instead. Borrow with &str or &String when the callee only needs temporary access.

A function that only counts characters should accept a string slice rather than consume a String. A function that stores or returns ownership may accept String. Choose from the operation's ownership need, not from a reflex to clone around a compiler error.

Clone is an explicit duplication operation. For String, it copies the string data, which may allocate. Deriving Copy is only appropriate for types whose values can be duplicated cheaply and without resource ownership semantics.

Rust · Worked example
fn count(s: &str) -> usize { s.chars().count() }

let name = String::from("Ada");
let n = count(&name);
println!("{name}: {n}");

The function borrows the string for the call. name remains owned by the caller and is still usable afterward.

Executed with Rust 1.85.1 in an isolated Linux container; the example assertions passed.

Follow-up: When should a function accept String instead of &str?

When it must take ownership, retain the text beyond the call, or return the same allocation as part of its result. A generic impl Into<String> may support multiple callers, but add that flexibility only when it improves a real API.

Read the source: The Rust Book: what is ownership? · The Rust Book: references and borrowing

↑ Back to questions

Question 2 of 9

Why can an immutable borrow block a later mutation?

References must not outlive the data they point to, and Rust prevents overlapping mutable access with other active references. End the read borrow before mutation, or redesign the operation so it does not need both access patterns at once.

A map lookup returns &Value, and the program then inserts into the same map. The reference may be invalidated by a mutation that reallocates or changes the map. Use the borrowed value to compute what you need, let the reference's last use end, then mutate.

Non-lexical lifetimes allow a borrow to end after its last use even before the enclosing block ends. This is a compile-time reasoning rule, not a runtime lock. Interior-mutability types such as RefCell move some checks to runtime and can panic on conflicting borrows.

Rust · Worked example
let length = if let Some(value) = map.get("item") {
    value.len()
} else {
    0
};
map.insert("length", length.to_string());

The borrowed map value is used to produce an owned usize; the reference is no longer needed when insert mutates the map.

Executed with Rust 1.85.1 in an isolated Linux container; the example assertions passed.

Follow-up: When is cloning the borrowed value the right choice?

When an independent owned value is genuinely needed beyond the borrow, and the copy cost is acceptable. Otherwise shorten the borrow scope or return the specific derived data the next step needs.

Read the source: The Rust Book: references and borrowing · The Rust Book: interior mutability

↑ Back to questions

Question 3 of 9

What does a lifetime annotation promise?

A lifetime annotation describes relationships among borrowed inputs and outputs. It does not extend an object's storage duration or keep a value alive. The caller must still provide data that outlives every returned reference.

A parser returns a slice into its input, so the result cannot outlive that input. A parser that constructs new text should return an owned String; adding a named lifetime to the signature does not make a local buffer safe to return by reference.

Lifetime elision rules infer common relationships, but a named lifetime is useful when the relationship is not obvious. Avoid reading 'a as a fixed number of seconds: it is a compile-time region relationship chosen by the caller and compiler.

Rust · Worked example
fn first_word<'a>(text: &'a str) -> &'a str {
    text.split_whitespace().next().unwrap_or("")
}

The returned slice is tied to the input borrow. The function cannot return a reference to a local String that is dropped at the end of the function.

Executed with Rust 1.85.1 in an isolated Linux container; the example assertions passed.

Follow-up: When should this function return String instead?

When the result cannot be represented as a slice into the input, or when the caller needs the result to outlive the input. The function must then create or obtain owned storage.

Read the source: The Rust Book: validating references with lifetimes · Rust Reference: lifetime elision

↑ Back to questions

Question 4 of 9

How do `Result`, `?`, and error types shape an API?

Return Result<T, E> for recoverable failure, use ? to propagate it, and choose an error type that preserves the distinctions callers must act on. Convert to presentation text at the application boundary rather than erasing error structure early.

A configuration loader may distinguish a missing file, malformed TOML, and an invalid application setting. A CLI can add the path and command context while retaining a source error. A library should avoid forcing callers to parse a formatted string to identify a condition.

? returns early after converting an error into the enclosing function's error type where a conversion exists. It does not log, retry, or recover. Avoid converting every error to a generic string if downstream code needs to make a different decision.

Rust · Worked example
fn load(path: &Path) -> Result<Config, LoadError> {
    let text = std::fs::read_to_string(path)?;
    let config = toml::from_str(&text)?;
    validate(config)
}

This compiles only when LoadError can convert both I/O and TOML errors, for example through explicit From implementations or an error-derivation crate. The example shows the propagation boundary, not a complete type definition.

Executed with Rust 1.85.1 in an isolated Linux container; the example assertions passed.

Follow-up: Where should an error become a user-facing message?

At the boundary that knows the operation and audience, such as a CLI command or HTTP handler. Keep lower-level errors structured so the boundary can choose the right message, status, and logging detail.

Read the source: The Rust Book: error handling · Rust Reference: question-mark operator

↑ Back to questions

Question 5 of 9

What do Send and Sync promise at a thread boundary?

Send means ownership of a value may safely move to another thread. Sync means a shared reference may safely cross threads: T is Sync exactly when &T is Send. These are safety contracts, not promises about business invariants or throughput.

A trait bound describes behavior a generic function requires. For concurrency, the standard library uses marker traits with no methods to express which values may cross thread boundaries. A struct generally acquires these auto traits from its fields. Rc<T> deliberately lacks them because its reference count is not synchronized; changing a wrapper name does not change that ownership contract.

Arc<T> synchronizes its ownership count, but it does not make arbitrary interior mutation safe. Arc<RefCell<T>> cannot substitute for a properly synchronized shared mutable value. Use a suitable synchronization primitive or keep the value on one thread.

Implementing an unsafe trait manually is a proof obligation. Safe callers and other unsafe code may rely on that implementation. An incorrect unsafe impl Send or Sync can make safe-looking callers invoke undefined behavior. Prefer types whose fields allow correct automatic derivation; never add an unsafe implementation merely to silence a compiler error.

Rust · Worked example
use std::rc::Rc;
use std::sync::Arc;

fn require_send<T: Send>() {}
fn require_sync<T: Sync>() {}

fn main() {
    require_send::<Arc<String>>();
    require_sync::<Arc<String>>();
    // require_send::<Rc<String>>(); // does not compile
}

The first two calls check the thread-boundary contracts at compile time. Uncommenting the Rc line must fail because its reference count cannot be transferred safely between threads. The example does not create threads or measure contention.

Positive sample ran; expected `Rc<String>` compile failure matched E0277 with Rust 1.85.1 in an isolated Linux container.

Follow-up: Can a mutex fix every Send or Sync error?

No. Understand the contained type and ownership boundary first. A mutex serializes access but cannot justify transferring a thread-affine resource or an invalid unsafe abstraction. Sometimes the correct design is message passing to the owning thread.

Read the source: Rustonomicon: Send and Sync · Rust std Arc thread safety

↑ Back to questions

Question 6 of 9

When do you need `Arc<Mutex<T>>`, and what does it cost?

Use shared ownership only when multiple owners are part of the design. Arc shares ownership across threads; Mutex provides synchronized mutable access. Prefer single ownership and message passing when they express the relationship more directly.

A metrics registry may be shared by worker threads and updated under a mutex. Every lock adds coordination, and holding a guard across unrelated work can serialize requests or create deadlocks. Keep the critical section small and define a lock order if multiple locks are required.

Arc makes reference counting thread-safe, but it does not make the contained value safe to mutate by itself. Rc<RefCell<T>> is for single-threaded shared ownership with runtime borrow checks; it is not a thread-safe substitute. Reference cycles can keep Rc allocations alive.

Rust · Worked example
let counts = Arc::new(Mutex::new(HashMap::<String, usize>::new()));
let worker_counts = Arc::clone(&counts);
std::thread::spawn(move || {
    let mut counts = worker_counts.lock().expect("counter lock poisoned");
    *counts.entry("ok".into()).or_default() += 1;
});

The shared map is protected while it is mutated. A production API should decide whether a poisoned mutex is fatal, recoverable, or evidence that the protected invariant may be broken.

Executed with Rust 1.85.1 in an isolated Linux container; the example assertions passed.

Follow-up: When would a channel be simpler?

When one task can own the state and other tasks can send commands or receive snapshots. This removes shared mutable access, though it still needs bounded queues, shutdown behavior, and an overload policy.

Read the source: The Rust Book: shared-state concurrency · The Rust Book: reference cycles

↑ Back to questions

Question 7 of 9

Does an `async fn` run as soon as it is called?

No. Calling an async function creates a future that describes work; the future progresses when it is polled by an executor, commonly through .await. Rust's async syntax does not select or provide the runtime that schedules I/O and tasks.

A service can accidentally construct a future and never await or spawn it, so the operation never runs. A Rust async runtime crate supplies the executor and often the I/O driver. Choose one runtime boundary for an application unless a concrete integration requires more.

At each .await, a future may yield so other tasks can progress. Blocking CPU work or a blocking system call on an executor thread can stall unrelated tasks. Move blocking work to an appropriate blocking pool or dedicated thread, and bound that pool.

Rust · Worked example
async fn lookup() -> Result<Record, Error> {
    client.get_record().await
}

// The application runtime must await or spawn lookup().

The function signature alone does not execute the lookup. The exact runtime API depends on the chosen runtime crate and is intentionally not presented as a standard-library API.

Executed with Rust 1.85.1 in an isolated Linux container; the example assertions passed.

Follow-up: How does dropping a future relate to cancellation?

Dropping a future stops the caller from polling that value, but external work may have its own cancellation and cleanup behavior. Use the library's cancellation contract and ensure partially completed side effects are safe.

Read the source: The Rust Book: futures and async syntax · Rust standard library: async keyword

↑ Back to questions

Question 8 of 9

What can the Rust type system not prove for you?

The compiler checks types, borrowing, and trait constraints, but it cannot prove that the program implements the intended business rule. Unit, integration, and documentation tests check those observable expectations.

A discount function can compile and still apply the wrong threshold. Test boundaries such as one cent below, at, and above the threshold. Unit tests can access private helpers in a module; integration tests exercise the public crate API from an external test target.

Use property tests or fuzzing where input space is large and invariants are clear, but keep a small set of readable examples for the business contract. Tests can also be wrong, so tie them to an independently stated expectation.

Rust · Worked example
#[test]
fn discount_starts_at_threshold() {
    assert_eq!(discount_for(9_999), 0);
    assert_eq!(discount_for(10_000), 500);
}

The numbers are illustrative. The test should derive its boundary and expected discount from the actual product rule, not copy implementation constants without an independent contract.

Executed with Rust 1.85.1 in an isolated Linux container; the example assertions passed.

Follow-up: When is an integration test preferable to a unit test?

When the risk lies in the public contract or interaction among modules, such as serialization, process exit behavior, or a consumer's ability to call the crate. Keep the unit test for a narrow algorithm where setup would obscure the rule.

Read the source: The Rust Book: writing automated tests · The Rust Book: test organization

↑ Back to questions

Question 9 of 9

How do you verify that a Rust optimization helped?

Use a representative benchmark and profiler, keep the toolchain and workload fixed, and compare repeated measurements. A successful compile says nothing about whether the change improved latency, throughput, allocation, or memory use.

An endpoint allocates many temporary strings. Benchmark the full transformation with realistic record sizes, profile to confirm allocation is material, then compare the changed version. If the request is dominated by network wait, a faster parser may not change observed latency.

Cargo profiles control optimization and debug information. Compare like with like, normally a release-style build for throughput. Avoid inferring speed from source-level operations or a single timing run affected by machine load.

Shell · Worked example
cargo test --release
cargo bench --bench parse_records

The fixture ran cargo test --release and cargo bench --bench parse_records against a generated deterministic parser workload. The benchmark command produced timing output, but this did not run a profiler or establish representative application performance. A useful comparison needs the real target and workload, the exact toolchain, and repeated measurements under comparable machine conditions.

Executed with Rust 1.85.1 in an isolated Linux container; the example assertions passed.

Follow-up: What if wall-clock latency is unchanged but CPU usage drops?

That can still matter for capacity or cost, but state the metric that improved. Check whether the request is now waiting on another bottleneck and whether throughput or tail latency changes under load.

Read the source: Cargo build profiles · Cargo bench command

↑ Back to questions

Salary context

Advertised salaries for these skills

These are Hacker News job posts that mention these technologies, not employee earnings or a measure of what a skill adds to pay. The amounts summarize annualized advertised minimums in approximate USD. Groups can overlap and differ in roles, locations and dates.

Rust posts

103 posts · 103 usable advertised salary amounts

2024-07-01 to 2026-10-14

$150,000

Median advertised minimum, approximate USD/year

Middle 50%: $115,000–$180,000

Explore Rust salaries → Browse matching job posts →
Currency and calculation basis

Approximate USD using ECB daily reference rates as of 2026-10-09. Original currencies and offer ranges remain on the source listings. These figures cover posts tagged Rust, rather than every job in the broader technology family.

↑ Back to questions

Keep exploring