--- title: "Nim Best Practices" description: "Idiomatic Nim: the partial case/underscore-insensitive identifier rule, the module export system, choosing proc/func/template/macro/iterator/method, ARC/ORC value semantics, exception-based error handling with the raises effect system, the implicit result variable, discard, and release/danger compilation flags." language: nim framework: null category: language_best_practices tags: - nim - best-practices - memory-management - arc-orc - module-system - error-handling - naming-conventions - metaprogramming - anti-patterns keywords: - nim identifier equality case insensitive underscore - proc vs func vs template vs macro nim - nim result variable implicit return - nim raises pragma effect system - nim discard discardable pragma - nim d:release vs d:danger compiler flags - nim sink parameter move semantics last_updated: "2026-07-07" difficulty: intermediate version: "Nim >= 2.0 (ARC/ORC default), stable line 2.2" related: - ./README.md - ./common_pitfalls.md - ./metaprogramming.md - ./concurrency.md - ./testing.md - ./interop_ffi.md - ../../core/devops/autonomous_least_privilege_environments.md - ../../core/principles/solid.md search_priority: high status: published --- # Nim Best Practices ## Introduction / Overview Nim reads like Python and, since version 2.0, behaves at runtime much more like modern C++ or Rust: values are moved and destroyed deterministically by compiler-inserted hooks rather than tracked by a background garbage collector. Writing idiomatic Nim means understanding a handful of semantics that have no close analogue in Python, JavaScript, or even Go - most critically, the language's partial case/underscore-insensitive identifier equality, the distinction between six different callable-like constructs (`proc`, `func`, `template`, `macro`, `iterator`, `method`), and the ARC/ORC value-semantics model that replaced the pre-2.0 default GC. This document assumes the ecosystem orientation in [`README.md`](./README.md) and focuses on writing correct, idiomatic Nim day to day. For an exhaustive catalogue of traps beyond what is covered here, see [`common_pitfalls.md`](./common_pitfalls.md); for deep macro/template technique, see [`metaprogramming.md`](./metaprogramming.md). **When to use this guidance**: You are writing or reviewing production Nim code - a library, a service, or a CLI tool - and need patterns verified against the current (2.0+) language semantics rather than pre-2.0 assumptions. ## Core Concepts - **Partial style-insensitivity**: Nim identifier equality ignores all underscores and is case-insensitive for every character *except the first*. This is a parser-level rule, not a linter suggestion - it affects what the compiler considers a name clash or a match. - **Deterministic destruction**: Under the ARC/ORC default, every value's lifetime is governed by compile-time-inserted `=destroy`/`=copy`/`=sink` hook calls, not a runtime tracing collector. - **Exceptions, tracked statically by an opt-in effect system**: `raise`/`try`/`except` is the default control-flow mechanism for errors; the `{.raises: [...].}` pragma lets the compiler prove which exceptions a routine can propagate. - **`result` is implicit**: any non-`void` `proc`/`func`/`method`/`converter`/`iterator` (context-dependent) has an implicitly declared `result` variable, pre-initialized to that type's default (zero) value. - **Nothing is silently discarded**: a call's return value must be explicitly consumed or `discard`-ed unless the routine is marked `{.discardable.}`. ## Best Practices ### 1. Naming Conventions and Nim's Partial Case/Underscore-Insensitivity **Rationale**: The Nim manual defines identifier equality precisely as: two identifiers are equal if their first characters are identical (case-sensitively) **and** the remainder of each identifier is equal after removing all underscores and lower-casing every other character. Formally, `a[0] == b[0] and a[1..^1].replace("_", "").toLowerAscii == b[1..^1].replace("_", "").toLowerAscii`. This means `myVariable`, `my_variable`, and `myvariable` are the *same identifier* to the compiler - not merely a style nit, a hard equality rule that determines redeclaration errors, overload matches, and import name clashes. `MyVariable`, by contrast, is a genuinely distinct identifier from `myVariable`, because the first character's case is compared exactly. ```nim let myVariable = 1 # let my_variable = 2 # compile error: redeclaration - this is the SAME identifier as myVariable let MyVariable = 3 # OK - distinct identifier, first letter's case differs ``` This rule is exactly why Nim's style convention exists and matters: use `camelCase` for procs, variables, and fields, and `PascalCase` for type names - the first-letter case difference is then the *only* thing separating a type name (`Foo`) from a conventionally-named local variable or constructor-like proc (`foo`), a pattern used throughout the standard library (`var s: Stack; let s = newStack()`-style code depends on this). Never rely on an underscore or internal capitalization alone to distinguish two identifiers (e.g., naming both `orderId` and `order_id` as "different" variables in the same scope) - that is a compile error waiting to happen, not a stylistic choice, and it is one of the most common mistakes an LLM makes when porting variable-naming habits from Python or JavaScript into Nim. **Why this works well in production**: Consistent `camelCase`/`PascalCase` usage sidesteps the ambiguity entirely - code that never depends on case/underscore variation to distinguish identifiers never triggers this rule's sharp edges. See [`common_pitfalls.md`](./common_pitfalls.md) for further worked examples of this rule interacting with imports and overloads. ### 2. The Module and Export System **Rationale**: `import` and `include` are semantically distinct and not interchangeable. `import` loads another file as a **module** and brings only its explicitly *exported* symbols into scope. `include` performs a raw **textual paste** of the target file's contents into the current file at compile time - no module boundary is created, and included files typically share the includer's scope directly (used for splitting one logical module across files, not for consuming a library). ```nim # inventory.nim proc reserveStock*(sku: string, qty: int): bool = result = qty > 0 proc internalHelper(sku: string): string = sku.toUpperAscii() ``` ```nim # main.nim import inventory discard reserveStock("SKU-1", 5) # OK: exported with * # discard internalHelper("SKU-1") # compile error: not exported, invisible outside its module ``` Every symbol (proc, type, variable, constant, field) is **private to its defining module by default**; appending `*` immediately after the name marks it as exported and visible to importers. A common re-export pattern gathers several internal modules behind one public "facade" module using `export`: ```nim # api.nim - re-exports selected internals as one public surface import ./inventory, ./billing export inventory, billing ``` **Why this works well in production**: Explicit `*` export makes a module's public API surface grep-able and intentional - nothing is accidentally public - and prevents the extremely common LLM-generated-code failure of importing a stdlib or third-party module and calling a symbol that exists in the source file but was never marked exported (the compiler reports it as simply undeclared, which is confusing until this rule is understood). See the dedicated pitfall in [`common_pitfalls.md`](./common_pitfalls.md). ### 3. `proc` vs `func` vs `template` vs `macro` vs `iterator` vs `method` **Rationale**: Nim has six callable-like constructs; defaulting to the wrong one either hides bugs (side effects in something that looks pure) or reaches for far more machinery than the problem needs (a `macro` where a `template` would do, or a `method` where a plain `proc` would do). | Construct | Dispatch / evaluation | Side effects | Typical use | |---|---|---|---| | `proc` | Ordinary compiled routine, statically resolved (or generic) | Allowed | The default for almost all functions | | `func` | Exactly a `proc`, sugar for `proc ... {.noSideEffect.}` | Compiler-enforced **none** - cannot mutate globals or call non-`func` side-effecting code | Pure computations; the compiler rejects the declaration if it actually has a side effect | | `template` | Compile-time, hygienic **syntactic substitution**, inlined at every call site before type-checking | Whatever the substituted code does | Simple inline code reuse, DSL sugar, avoiding call overhead, injecting boilerplate | | `macro` | Compile-time, operates on and returns the **AST** (`NimNode`) directly | Whatever the generated code does | Genuine compile-time code generation/transformation that a template's simple substitution cannot express - see [`metaprogramming.md`](./metaprogramming.md) | | `iterator` | Lazily produces a sequence of values via `yield`, driven by a `for` loop (or, if `{.closure.}`, a first-class iterator value) | Allowed | Custom traversal logic, lazy/multi-step generation without building an intermediate collection | | `method` | **Runtime, dynamic dispatch** based on an object's runtime type; requires participating types to inherit from a common (typically `RootObj`-derived) base | Allowed | Genuine runtime polymorphism over a class hierarchy known only at runtime | ```nim func square(x: int): int = x * x # compiler-verified pure proc logAndSquare(x: int): int = # proc: side effects (logging) permitted echo "squaring ", x x * x template unless(cond: bool, body: untyped): untyped = if not cond: body # pasted inline at each call site, no proc-call overhead iterator countTo(n: int): int = var i = 0 while i <= n: yield i inc i type Shape = ref object of RootObj Circle = ref object of Shape radius: float method area(s: Shape): float {.base.} = 0.0 method area(s: Circle): float = 3.14159 * s.radius * s.radius ``` **Why this works well in production**: prefer **static dispatch** - plain `proc` overloading or generics with a type-class-style constraint - over `method` as the default polymorphism mechanism. Static dispatch is resolved and inlinable at compile time, avoids the runtime type-information and vtable-like overhead `method` requires, and does not force every participating type into a shared inheritance hierarchy; reach for `method` only when the set of concrete types genuinely is not known until runtime (e.g., a plugin registry). Prefer `func` over `proc` wherever a routine is genuinely pure - the compiler then guarantees purity for you, catching accidental side effects at compile time rather than in production. Prefer `template` over `macro` whenever simple substitution suffices; a `macro`'s full AST access is powerful but harder to read, harder to debug, and unnecessary for most inlining/boilerplate-elimination use cases - see [`metaprogramming.md`](./metaprogramming.md) for the deep-dive on both. ### 4. Memory Management: ARC/ORC and Value Semantics **Rationale**: Since Nim 2.0, **ORC (ARC plus a back-up cycle collector) is the default memory management mode**, replacing the deferred reference-counting tracing GC (`--mm:refc`) that was the default throughout Nim's 1.x line. Under ARC/ORC, the compiler inserts calls to compiler-synthesized (or user-overridden) `=destroy`, `=copy`, and `=sink` hooks at well-defined points - variable declaration, assignment, and routine calls - rather than relying on a background collector to reclaim memory eventually. The practical consequence: ordinary `seq`, `string`, and `object` values behave as **value types with move semantics**, not as hidden references. Passing a `seq` into a function that takes ownership, or returning one from a function, is compiled into a move (a cheap pointer/length transfer with the source invalidated) whenever the compiler can prove the source is not used again, and a real copy only when it cannot. ```nim proc consume(data: sink seq[int]) = echo data.len # `data` is owned here; the compiler moves rather than copies when possible var big = @[1, 2, 3, 4, 5] consume(big) # moved into `consume` - no deep copy, `big` should not be read afterward ``` Most ordinary Nim code needs **no manual `ref`/pointer juggling at all**: define plain `object` types, pass them by value or by `var`/`sink` parameter, and let the compiler manage lifetime. Reach for `ref object` (heap-allocated, ORC-cycle-collected) specifically when you need: genuinely shared mutable state across multiple owners, an explicit heap allocation (large objects you do not want copied, or objects whose address must stay stable), or a recursive/cyclic data structure (trees with parent pointers, doubly linked lists, graphs) where value semantics cannot express the shape at all. ```nim type Node = ref object # ref: heap-allocated, shared, ORC collects cycles automatically value: int next: Node # a plain `object` (value type) could not express this self-reference ``` **Why this works well in production**: deterministic destruction means no GC pause to reason about under load, predictable latency, and RAII-style resource cleanup (closing files, releasing locks) exactly at scope exit - while still freeing the programmer from manual `malloc`/`free` bookkeeping for the common case. `--mm:refc` remains available (`nim c --mm:refc`) for legacy codebases or specific compatibility needs, but new code should target ORC (the default) or ARC explicitly. ### 5. Error Handling **Rationale**: Nim's default error-handling mechanism is exceptions - `raise SomeError.newException("message")`, caught with `try`/`except`/`finally` - not return-value-based error codes. This is a deliberate design choice distinct from Rust's `Result`-everywhere or Go's `if err != nil`, and it is easy for an LLM trained heavily on Rust/Go conventions to under-use. ```nim type InsufficientInventoryError = object of CatchableError proc reserveStock(available, requested: int) = if requested > available: raise newException(InsufficientInventoryError, "requested more than available") try: reserveStock(available = 2, requested = 10) except InsufficientInventoryError as e: echo "reservation failed: ", e.msg finally: echo "reservation attempt complete" ``` For statically constraining which exceptions a routine may raise, use the `{.raises.}` effect-tracking pragma - an empty list is a compile-time guarantee that a routine raises nothing: ```nim proc parsePositive(s: string): int {.raises: [ValueError].} = result = parseInt(s) # parseInt itself may raise ValueError; anything else is a compile error if result <= 0: raise newException(ValueError, "must be positive") proc pureAdd(a, b: int): int {.raises: [].} = a + b # compiler verifies this genuinely cannot raise ``` For **expected**, non-exceptional absence-of-value or recoverable-failure cases, prefer an explicit type over exceptions: the standard library's `std/options` (`Option[T]`, `some`/`none`) for "a value might legitimately be absent," and the widely used community `results` package (`arnetheduck/nim-results`, installable via `nimble install results`) for a `Result[T, E]` value-or-error type modeled closely on Rust's `Result`. Prefer exceptions for genuinely exceptional, rarely-handled-at-the-call-site failures (a malformed config file aborting startup); prefer `Option`/`Result` for expected, frequently-handled-at-the-call-site outcomes (a lookup that legitimately may miss, a validation that legitimately may fail) where forcing the caller to handle both branches at the type level is valuable. ```nim import std/options proc findUser(id: int): Option[string] = if id == 1: some("alice") else: none(string) let user = findUser(2) if user.isSome: echo user.get() else: echo "no such user" ``` ### 6. The Implicit `result` Variable **Rationale**: Every `proc`, `func`, `method`, or `converter` with a non-`void` return type has an **implicitly declared variable named `result`**, pre-initialized to that type's default (zero) value - `0` for integers, `""` for strings, `false` for `bool`, `nil` for `ref`/`ptr`, an empty `seq`/default-initialized `object` otherwise. If control flow reaches the end of the routine body without an explicit `return`, the routine returns whatever `result` currently holds. ```nim proc classify(x: int): string = if x < 0: result = "negative" elif x == 0: result = "zero" else: result = "positive" # no explicit `return` needed - `result`'s final value is returned ``` Two mistakes are common here, both worth checking for in generated code: **forgetting `result` exists** and declaring a redundant local variable that is then explicitly `return`-ed instead (harmless, but non-idiomatic and easy to get wrong when refactoring), and **assuming `result` starts uninitialized** - it does not; it always starts at the type's default zero value, so a routine that fails to assign it on every path silently returns that zero value rather than erroring, which can mask logic bugs. ```nim # Bug: forgot to set result on one branch - silently returns "" (string default) instead of erroring proc riskyClassify(x: int): string = if x > 0: result = "positive" # missing branch for x <= 0 - result stays "" here, easy to overlook in review ``` ### 7. `discard` and the Discardable-by-Default Rule **Rationale**: Nim requires every call whose return value is not used to be explicitly wrapped in a `discard` statement - an unused, non-`void` return value is a compile-time **error** (not merely a warning), unless the callee is marked `{.discardable.}`. This exists to catch the extremely common bug class of calling a function for its return value and then accidentally ignoring it (a failed-write check, an error code, a builder-pattern result used only for its side effect). ```nim proc computeChecksum(data: string): int = data.len # ordinary proc, return value matters # computeChecksum("abc") # compile error: value of type `int` has to be used (or discarded) discard computeChecksum("abc") # OK: explicitly acknowledges the value is intentionally unused proc logEvent(msg: string): bool {.discardable.} = echo msg result = true logEvent("startup complete") # OK without discard - explicitly opted in via {.discardable.} ``` **Why this works well in production**: mark a proc `{.discardable.}` only when ignoring its result is a genuinely valid, common usage pattern (logging helpers, builder-style chained calls) - for everything else, the compiler-enforced `discard` requirement turns a whole class of "I forgot to check this" bugs into a compile error instead of a silent runtime mistake. ### 8. Compilation Flags for Development vs. Production **Rationale**: Nim's runtime safety checks (bounds checking, integer overflow checking, nil dereference checking, and more) are **on** by default and during ordinary `-d:release` builds; only `-d:danger` strips them all for maximum throughput, and the memory-management mode and threading support are independent, explicitly selectable flags. | Flag | Effect | |---|---| | (no flag, debug build) | All runtime checks on; no optimization; best compile-time error messages and stack traces | | `-d:release` | Enables compiler optimizations; **keeps** runtime safety checks (bounds, overflow, nil) enabled - the right default for production binaries that still want safety nets | | `-d:danger` | Enables optimizations **and disables all runtime safety checks** (bounds, overflow, nil, assertions) for maximum performance - use only after the code is proven correct by tests, and treat it the same way you would treat disabling `-fno-stack-protector` in C | | `--mm:orc` | Explicitly select ORC (already the default since Nim 2.0) | | `--mm:arc` | ARC without the cycle collector - smaller/faster, but leaks genuine reference cycles; avoid with the stdlib async dispatcher, which creates cycles | | `--mm:refc` | The pre-2.0 default deferred reference-counting tracing GC; mainly relevant for legacy-codebase compatibility | | `--threads:on` | Enables the threading library/runtime support (implicitly on by default in recent Nim versions for most configurations, but still worth setting explicitly for clarity and for older-Nim compatibility) | ``` # nim.cfg or config.nims at the project root - checked in, not typed per-invocation --mm:orc --threads:on # then simply: # nim c -d:release src/orders.nim (safety checks kept, optimized) # nim c -d:danger src/orders.nim (all runtime checks stripped, fastest) ``` **Why this works well in production**: put these flags in a project-level `nim.cfg`/`config.nims` rather than expecting every developer or CI job to remember and retype them - this guarantees every build (local, CI, release) uses the same memory-management mode and threading configuration, and makes the release-vs-danger safety trade-off an explicit, reviewed project decision rather than an individual invocation's accident. See [`concurrency.md`](./concurrency.md) for the full implications of `--threads:on` combined with ARC/ORC. ## Common Pitfalls & Anti-Patterns This section lists a starter set; [`common_pitfalls.md`](./common_pitfalls.md) is the canonical, exhaustive reference and should be consulted before trusting any nontrivial generated Nim code. ### Pitfall: Confusing Inclusive `..` with Exclusive `..<` Ranges **Problem**: `0..5` is an **inclusive** range (`0, 1, 2, 3, 4, 5` - six elements), while `0..<5` is **exclusive** of the upper bound (`0, 1, 2, 3, 4` - five elements). Code ported from a language where slicing/ranges default to exclusive (Python's `range(5)`, Rust's `0..5`) frequently uses `0..n` in Nim expecting `n` exclusive semantics, silently processing one extra element. **Recommended approach**: Use `0..` everywhere, more upfront design | Explicit but simple (`if err != nil`) | Context-dependent | | Compile-time safety guarantees | Effect system opt-in (`{.raises.}`, `func`), not enforced by default | Strongest (ownership + exhaustive `Result`) | Weaker | Rust | | Metaprogramming power | Very high (`macro` full AST access) | High but more ceremony (proc macros) | Low (no generative macros) | Nim | | Iteration speed for CRUD-style services | Fast (Python-like syntax, fast compiles) | Slower (borrow checker friction) | Fast | Nim / Go | | Ecosystem breadth | Smaller | Larger, fast-growing | Very large | Rust / Go | **Choose Rust** (see [`../rust/README.md`](../rust/README.md)) when compile-time-enforced memory/thread safety is non-negotiable and the team can absorb the ownership learning curve. **Choose Go** (see [`../go/README.md`](../go/README.md)) when team velocity and a very large existing ecosystem outweigh Nim's metaprogramming flexibility and multi-backend compilation. ## Related Documentation - [Nim Language Overview & Ecosystem](./README.md) - [Nim Common Pitfalls & Anti-Patterns](./common_pitfalls.md) - [Nim Metaprogramming](./metaprogramming.md) - [Nim Concurrency](./concurrency.md) - [Nim Testing](./testing.md) - [Nim FFI/Interop](./interop_ffi.md) - [SOLID Principles](../../core/principles/solid.md) - [Autonomous, Least-Privilege, Portable Execution Environments](../../core/devops/autonomous_least_privilege_environments.md) ## Glossary - **Partial style-insensitivity**: Nim's identifier equality rule - case-sensitive on the first character only, case-insensitive and underscore-ignoring thereafter. - **`func`**: Sugar for `proc ... {.noSideEffect.}` - a compiler-enforced pure routine. - **`sink` parameter**: A parameter declared `sink T` that takes ownership of its argument, enabling the compiler to move rather than copy when it can prove the source is not reused. - **`=destroy`/`=copy`/`=sink`**: The compiler-synthesizable (or user-overridable) hook procs that implement ARC/ORC's deterministic destruction, copying, and move semantics for a type. - **Effect system**: Nim's compile-time tracking of a routine's side effects and raised exceptions, surfaced via pragmas like `{.raises.}` and `{.noSideEffect.}`. - **`result`**: The implicitly declared, zero-initialized variable available in any non-`void` routine, returned automatically if no explicit `return` executes. - **`{.discardable.}`**: A pragma opting a routine's return value out of the otherwise-mandatory `discard` requirement. - **`openArray[T]`**: A parameter-only, copy-free view type accepting either a `seq[T]` or an `array[_, T]` argument. --- **Maintenance note**: When updating this document, also update `last_updated` and re-verify the ARC/ORC default, effect-system pragma syntax, and any package (`results`, `nimble`, `atlas`) maturity claims against the current [Nim manual](https://nim-lang.org/docs/manual.html) and [Nim blog](https://nim-lang.org/blog.html).