Aiming for Jarvis, Creating D.A.N.I.

Tutorial 7: Error handling (or, Accepting that things will inevitably go wrong)

If you've been working with software long enough, you know that "perfect code" is a myth. Code fails. Hardware flakes out. The network goes on holiday just when you need it most. In Skink, we don't pretend these things don't happen—we face them head-on. That's what error handling is for. It's not pessimism; it's engineering.

The error type: It's not a failure, it's a notification

When a function might run into a problem, we don't just throw up our hands and crash. We return an error type alongside our data. Think of it as a signal to the caller: "Hey, I couldn't finish what you asked, and here's why."

module main

import "std/errors"

fn divide(a: float, b: float) -> (float, error) {
    if b == 0.0 {
        // We return a result and an error object
        return 0.0, errors.New("division by zero")
    }
    // nil is the "everything is fine" state
    return a / b, nil
}

Checking errors: Don't ignore the warning signs

Whenever you see a function that returns an error, treat it like a flashing dashboard warning light. Check it, or be prepared for the consequences (usually a spectacular crash).

fn main() -> int {
    result, err := divide(10.0, 2.0)
    if err != nil {
        // If we got an error, handle it gracefully
        print("Failed: {err.String()}")
        return 1
    }
    print("Result: {result}")
    return 0
}

The ? operator: The "let someone else deal with it" shortcut

Sometimes you don't want to handle an error right where it happens. You might want to pass it up the chain to a function that's better equipped to deal with it (or just let the main loop log it and move on). The ? operator is your friend here. It says: "If this returned an error, return it from this function right now. Otherwise, keep going."

fn compute() -> (float, error) {
    // If divide fails, compute returns immediately with the error
    x := divide(10.0, 2.0)?
    y := divide(x, 0.0)?
    
    return x + y, nil
}

It’s a massive time-saver, and it keeps your code from becoming a nesting-doll pile of if err != nil blocks. Just remember: it only works in functions that return an error themselves.

Exercises

  1. Square Root Logic: Write a sqrt(x: float) -> (float, error) function. It should return an error if the input is less than zero. Math isn't optional, after all.
  2. The Chain: Create a calculate function that calls your sqrt function and then uses the result in a divide operation. Use ? to handle the errors.
  3. Friendly Feedback: Instead of just printing the error string, print a user-friendly message that explains why the operation failed. Your future self (or a user) will thank you.

Next

When you're ready to make your code walk and chew gum at the same time, we'll talk about Concurrency.