Coming from Go: Variable shadowing with ok: bool and the or_return logging paradigm

Hey everyone,

I’m currently building a 2D game engine in Odin. Coming from a Go background, my default error-handling pattern for procedures is returning (Value, ok: bool)—or just ok: bool for simple checks.

In Go, I was used to reusing err across multiple calls in the same scope using short variable declarations (:=), as long as at least one new variable was being declared on the left side:

`Go// Valid Go code
val1, err := stepOne()
if err != nil { return err }

val2, err := stepTwo() // Reuses ‘err’ because val2 is new
if err != nil { return err }`

However, in Odin, trying a similar pattern throws a variable shadowing error because Odin strictly forbids re-declaring identifiers in the same scope with :=:

`Fragment kodu// :x: Odin Compiler Error: Variable shadowing / ‘ok’ already declared
val1, ok := my_proc()
if !ok do return

val2, ok := my_proc()
if !ok do return`

I understand why Odin prevents variable shadowing (to eliminate a huge class of subtle bugs), but working around it raised a few questions for me regarding idiomatic Odin design:

1. Re-using variables with = vs :=

If a procedure returns only ok: bool, pre-declaring ok once and reusing it with = is simple and clean:

`Fragment kodu// Pre-declaring works great for single-value returns:
ok: bool

ok = simple_check_one()
if !ok do return

ok = simple_check_two()
if !ok do return`

However, if procedures return multiple values (e.g., (Texture, i32, i32, bool)), using = requires pre-declaring all the variables upfront just to avoid shadowing ok. Adding call-site logging to each check quickly gets verbose:

`Fragment kodu// Works, but pre-declaring everything just to log at the call-site gets verbose:
tex1, tex2: ^sdl2.Texture
w1, h1, w2, h2: i32
ok: bool

tex1, w1, h1, ok = load_texture(“player.png”)
if !ok {
log.error(“Failed to load player texture at startup”)
return
}

tex2, w2, h2, ok = load_texture(“enemy.png”)
if !ok {
log.error(“Failed to load enemy texture at startup”)
return
}`

2. The or_return logging trade-off

If I use or_return to avoid managing the ok variable altogether, it stays very clean:

Fragment kodutex1, w1, h1 := load_texture("player.png") or_return tex2, w2, h2 := load_texture("enemy.png") or_return

However, using or_return means I can’t easily print a contextual log message at the callsite on failure before returning. It seems to force a design where leaf procedures are expected to log errors internally, while parent procedures focus purely on control flow.

My Questions for the Community:

  1. Is pushing log responsibility down to leaf procedures (or using context.logger) the intentional design philosophy when leaning into or_return?
  2. What is the most idiomatic way in Odin to handle sequential multi-value (Value, bool) calls when you do want to log contextual details at the callsite without creating deeply nested code or pre-declaring heaps of variables?

Thanks!

Try the following:

some_val, ok := some_proc()
if !ok // handle the error
another_val, ok2 := some_proc_2()
if !ok2 // handle the error

It’s a simple way to

I actually thought of that before you mentioned it! Coming from Go, it looked a bit strange to me at first, but after weighing the options, it really does seem like the best solution so far.

you can also do this, which can sometimes be very handy.

	if val, ok := proc1(); ok {
		val += 1
	}

	if val, ok := proc2(); !ok {
	}

1 Like

That works, but you have to remember to assign the local variable to something that exists outside the if block scope.

val := 0
if tempval, ok := test1(); ok {
    val += tempval
}
fmt.println("Val is: ", val)
if tempval, ok := test2(); !ok {
    val -= tempval
}
fmt.println("Val is now: ", val)

i never do this. i only use an initial statement when i explicitly want the block scope.

Thanks! I’m aware of the if initializer syntax, but in my case, I need val to stay in scope and be accessible after the check, rather than locked inside the if block.

1 Like

I prefer to use the named return value for errors, and just define intermediate variables before it is used.

It also makes it clear what the variable is, so if I have z: ^Z above an error check, it’s pretty glaring that I might need a defer to free it.

I also tend to not do partial initialisations of named returns for the happy path, so when (not if) I forget to check an error, my data structure is consistently zero-initialised.

foo :: proc() -> (thing: Thing, ok: bool) {
    x: X
    if x, ok = get_x(); !ok { /* do logging */ return }

    y: Y
    if y, ok = get_y(); !ok { /* ... */ return }
    
    z: ^Z
    if z, ok = get_z(); !ok { /* ... */ return }
    defer z_free(z)
    /* do something with z */

    return Thing { x, y }, true
}

Also, naked returns for error cases look/feel very similar to or_return, so when I look at either it just means “error path”.

2 Likes