proj-oot-ootErrorNotes4

[1]

"

Error Set Type

An error set is like an enum. However, each error name across the entire compilation gets assigned an unsigned integer greater than 0.

...

const FileOpenError? = error { AccessDenied?, OutOfMemory?, FileNotFound?, };

...

Error Union Type

An error set type and normal type can be combined with the ! binary operator to form an error union type.

...

pub fn parseU64(buf: []const u8, radix: u8) !u64 { var x: u64 = 0;

    for (buf) |c| {
        const digit = charToDigit(c);
        if (digit >= radix) {
            return error.InvalidChar;
        }
        // x *= radix
        if (@mulWithOverflow(u64, x, radix, &x)) {
            return error.Overflow;
        }
        // x += digit
        if (@addWithOverflow(u64, x, digit, &x)) {
            return error.Overflow;
        }
    }
    return x;}

test "parse u64" { const result = try parseU64("1234", 10); ... "

 Notice the return type is !u64. This means that the function either returns an unsigned 64 bit integer, or an error. We left off the error set to the left of the !, so the error set is inferred.

Within the function definition, you can see some return statements that return an error, and at the bottom a return statement that returns a u64. Both types implicitly cast to error!u64.

What it looks like to use this function varies depending on what you're trying to do. One of the following:

    You want to provide a default value if it returned an error.
    If it returned an error then you want to return the same error.
    You know with complete certainty it will not return an error, so want to unconditionally unwrap it.
    You want to take a different action for each possible error.

If you want to provide a default value, you can use the catch binary operator:

fn doAThing(str: []u8) void { const number = parseU64(str, 10) catch 13; ... }

...

Let's say you wanted to return the error if you got one, otherwise continue with the function logic:

fn doAThing(str: []u8) !void { const number = parseU64(str, 10) catch

errreturn err;
    // ...}

There is a shortcut for this. The try expression:

fn doAThing(str: []u8) !void { const number = try parseU64(str, 10); ... }

...

 Maybe you know with complete certainty that an expression will never be an error. In this case you can do this:

const number = parseU64("1234", 10) catch unreachable;

Here we know for sure that "1234" will parse successfully. So we put the unreachable value on the right hand side. unreachable generates a panic in Debug and ReleaseSafe? modes and undefined behavior in ReleaseFast? mode.

...

Finally, you may want to take a different action for every situation. For that, we combine the if and switch expression:

fn doAThing(str: []u8) void { if (parseU64(str, 10))

number{
        doSomethingWithNumber(number);
    } else |err| switch (err) {
        error.Overflow => {
            // handle overflow...
        },
        // we promise that InvalidChar won't happen (or crash in debug mode if it does)
        error.InvalidChar => unreachable,
    }}

...

The other component to error handling is defer statements. In addition to an unconditional defer, Zig has errdefer, which evaluates the deferred expression on block exit path if and only if the function returned with an error from the block.

...

    These primitives give enough expressiveness that it's completely practical to have failing to check for an error be a compile error. If you really want to ignore the error, you can add catch unreachable and get the added benefit of crashing in Debug and ReleaseSafe modes if your assumption was wrong.
    Since Zig understands error types, it can pre-weight branches in favor of errors not occuring. Just a small optimization benefit that is not available in other languages.

...

Inferred Error Sets

Because many functions in Zig return a possible error, Zig supports inferring the error set. To infer the error set for a function, use this syntax:

test.zig

With an inferred error set pub fn add_inferred(comptime T: type, a: T, b: T) !T { var answer: T = undefined; return if (@addWithOverflow(T, a, b, &answer)) error.Overflow else answer; }

...

When a function has an inferred error set, that function becomes generic and thus it becomes trickier to do certain things with it, such as obtain a function pointer, or have an error set that is consistent across different build targets. Additionally, inferred error sets are incompatible with recursion.

These limitations may be overcome in a future version of Zig. "

---

" That's because the ? operator does a min more than the code snippet in the draft design shows:

    if result.err != nil {
	    return result.err
    }
    use(result.value)

Instead it does the following:

    if result.err != nil {
	    return ResultErrorType.from(result.err)
    }
    use(result.value)

This means that a very common way to handle errors in Rust is to define your own Error type consumes other errors and adds context to them. " [2]

---

" To make this concrete, in Rust you end up with code that looks like this:

fn do_it(filename: &str) -> Result { let stat = match fs::metadata(filename) { Ok(result) => { result }, Err(err) => { return Err(err); } };

    let file = match File::open(filename) {
        Ok(result) => { result },
        Err(err) => { return Err(err); }
    };
    /* ... */
    Ok(())}

Already, this is pretty good: it’s cleaner and more robust than multiple return values, return sentinels and exceptions — in part because the type system helps you get this correct. But it’s also verbose, so Rust takes it one step further by introducing the propagation operator: if your function returns a Result, when you call a function that itself returns a Result, you can append a question mark on the call to the function denoting that upon Ok, the result should be unwrapped and the expression becomes the unwrapped thing — and upon Err the error should be returned (and therefore propagated). This is easier seen than explained! Using the propagation operator turns our above example into this:

fn do_it_better(filename: &str) -> Result { let stat = fs::metadata(filename)?; let file = File::open(filename)?;

    /* ... */
    Ok(())}

This, to me, is beautiful: it is robust; it is readable; it is not magic. And it is safe in that the compiler helps us arrive at this and then prevents us from straying from it. " [3]

---

[4]

---

carussell on Aug 19, 2017 [-]

All this and handling overflow still doesn't make the list. Had it been the case that easy considerations for overflow were baked into C back then, we probably wouldn't be dealing with hardware where handling overflow is even more difficult than it would have been on the PDP-11. (On the PDP-11, overflow would have trapped.) At the very least, it would be the norm for compilers to emulate it whether there was efficient machine-level support or not. However, that didn't happen, and because of that, even Rust finds it acceptable to punt on overflow for performance reasons.

---

" I would rather that methods that throw exceptions would return result type instead, so error handling would be explicit. " [5]

---

maybe it is a compile-time error if the top-level program doesn't handle all exceptions that can reach it? hmmm that would make scripting pretty annoying though. Maybe this should be 'strict' mode only.

---

" Why should I have written ZeroMQ? in C, not C++ (part I)

Naturally, when I started ZeroMQ? project back in 2007, I've opted for C++. The main reasons were:

    Library of data structures and algorithms (STL) is part of the language. With C I would have to either depend on a 3rd party library or had to write basic algorithms of my own in 1970's manner.
    C++ enforces some basic consistency in the coding style. For example, having the implicit 'this' parameter doesn't allow to pass pointer to the object being worked on using several disparate mechanisms as it often happens to be the case with C projects. Same applies to explicit marking of member variables as private and many other features of the language.
    This point is actually a subset of the previous one, but it's worth of explicit mention: Implementing virtual functions in C is pretty complex and tends to be slightly different for each class which makes understanding and managing the code a pain.
    And finally: Everybody loves destructors being invoked automatically at the end of the block.

Now, almost 5 years later, I would like to publicly admit that using C++ was a poor choice and explain why I believe it is so.

First, it's important to take into account that ZeroMQ? was intended to be a piece of infrastructure with continuous uptime. It should never fail and never exhibit undefined behaviour. Thus, the error handling was of utmost importance. It had to be very explicit and unforgiving.

C++ exceptions just didn't fill the bill. They are great for guaranteeing that program doesn't fail just wrap the main function in try/catch block and you can handle all the errors in a single place.

However, what's great for avoiding straightforward failures becomes a nightmare when your goal is to guarantee that no undefined behaviour happens...

With C, the raising of the error and handling it are tightly couped and reside at the same place in the source code. This makes it easy to understand what happens if error happens...With C++ you just throw the error...The problem with that is that you have no idea of who and where is going to handle the exception.

...

Consider the case when the exception is not handled in the function that raises it. In such case the handling of the error can happen anywhere, depending on where the function is called from.

While the possibility to handle the exceptions differently in different contexts may seem appealing at the first sight, it quickly turns into a nightmare.

As you fix individual bugs you'll find out that you are replicating almost the same error handling code in many places. Adding a new function call to the code introduces that possibility that different types of exceptions will bubble up to the calling function where there are not yet properly handled. Which means new bugs.

If you don't give up on the "no undefined behaviour" principle, you'll have to introduce new exception types all the time to distinguish between different failure modes. However, adding a new exception type means that it can bubble up to different places. Pieces of code have to be added to all those places, otherwise you end up with undefined behaviour.

At this point you may be screaming: That's what exception specifications are for!

Well, the problem is that exception specifications are just a tool to handle the problem of exponential growth of the exception handling code in a more systematic manner, but it doesn't solve the problem itself. It can even be said it makes it worse as now you have to write code for the new exception types, new exception handling code *and* new exception specifications.

Taking the problems described above into account I've decided to use C++ minus exceptions. That's exactly how ZeroMQ? and Crossroads I/O looks like today.

Unfortunately, the problems don't end up here...

Consider what happens when initialisation of an object can fail. Constructors have no return values, so failure can be reported only by throwing an exception. However, I've decided not to use exceptions. So we have to go for something like this:

class foo { public: foo (); int init (); ... };

When you create an instance of the class, constructor is called (which cannot fail) and then you explicitly call init function (which can fail).

This is more complex that what you would do with C:

struct foo { ... };

int foo_init (struct foo *self);

However, the really bad thing about the C++ version of the code is what happens when developers put some actual code into the constructor instead of systematically keeping the constructors empty.

If that's the case a special new object state comes into being. It's the 'semi-initialised' state when object has been constructed but init function haven't been called yet. The object (and specifically the destructor) should be modified in such a way as to decently handle the new state. Which in the end means adding new condition to every method.

Now you say: But that's just a consequence of your artificial restriction of not using exceptions! If exception is thrown in a constructor, C++ runtime cleans the object as appropriate and there is no 'semi-initalised' state whatsoever!

Fair enough. However, it's beside the point. If you start using exceptions you have to handle all the exception-related complexity as described in the beginning. And that is not a reasonable option for an infrastructure component with the need to be very robust in the face of failures.

Moreover, even if initialisation wasn't a problem, termination definitely is. You can't really throw exceptions in the destructor. Not because of some self-imposed artificial restrictions but because if the destructor is invoked in the process or unwinding the stack and it happens to throw an exception, it crashes the entire process.

Thus, if termination can fail, you need two separate functions to handle it:

class foo { public: ... int term (); ~foo (); };

Now we are back to the problem we've had with the initialisation: There's a new 'semi-terminated' state that we have to handle somehow, add new conditions to individual member functions etc.

...

Compare the above to the C implementation. There are only two states. Not initialised object/memory where all the bets are off and the structure can contain random data. And there is initialised state, where the object is fully functional. Thus, there's no need to incorporate a state machine into the object...

Now consider what happens when you add inheritance to the mix. C++ allows to initialise base classes as a part of derived class' constructor. Throwing an exception will destruct the parts of the object that were already successfully initialised...

However, once you introduce separate init functions, the number of states starts to grow. In addition to uninitialised, semi-initialised, initialised and semi-terminated states you encounter combinations of the states. As an example you can imagine a fully initialised base class with semi-initialised derived class.

With objects like these it's almost impossible to guarantee predictable behaviour. There's a lot of different combinations of semi-initialised and semi-terminated parts of the object and given that failures that cause them are often very rare the most of the related code probably goes into the production untested.

To summarise the above, I believe that requirement for fully-defined behaviour breaks the object-oriented programming model. The reasoning is not specific to C++. It applies to any object-oriented language with constructors and destructors.

Consequently is seems that object-oriented languages are better suited for the environments where the need for rapid development beats the requirement for no undefined behaviour.

There's no silver bullet here. The systems programming will have to live on with C.

-- http://250bpm.com/blog:4

---

"

Clojure 1.10 focuses on two major areas: improved error reporting and Java compatibility.

Error reporting at the REPL now categorizes errors based on their phase of execution (read, macroexpand, compile, etc). Errors carry additional information about location and context as data, and present phase-specific error messages with better location reporting. This functionality is built into the clojure.main REPL, but the functionality is also available to other REPLs and tools with the ability to use and/or modify the data to produce better error messages. "

---

the_duke 2 days ago [-]

That RFC was accepted and merged quite a while ago!

https://github.com/rust-lang/rfcs/pull/2504

reply

babyloneleven 2 days ago [-]

The thing is that the description method in the Error trait in the stdlib is broken. I can't find the post on the internals.rust-lang.org atm, but the author of Failure has a plan for backwards compatibly fixing it and eventually moving (parts of) failure into the stdlib.

The other thing about Rust error handling is the amount of boilerplate to convert between errors. Error-chain and failure are two iterations on how to deal with it. Failure seems to be the current best practices.

reply

dralley 2 days ago [-]

https://github.com/rust-lang/rust/issues/53487

reply

---

" In Rust 1.32.0, we've added a new macro, dbg!, for this purpose:

fn main() { let x = 5;

    dbg!(x);}

If you run this program, you'll see:

[src/main.rs:4] x = 5

You get the file and line number of where this was invoked, as well as the name and value. Additionally, println! prints to the standard output, so you really should be using eprintln! to print to standard error. dbg! does the right thing and goes to stderr. "

" Consider this version using dbg!:

fn factorial(n: u32) -> u32 { if dbg!(n <= 1) { dbg!(1) } else { dbg!(n * factorial(n - 1)) } }

We simply wrap each of the various expressions we want to print with the macro. We get this output instead:

[src/main.rs:3] n <= 1 = false [src/main.rs:3] n <= 1 = false [src/main.rs:3] n <= 1 = false [src/main.rs:3] n <= 1 = true [src/main.rs:4] 1 = 1 [src/main.rs:5] n * factorial(n - 1) = 2 [src/main.rs:5] n * factorial(n - 1) = 6 [src/main.rs:5] n * factorial(n - 1) = 24 [src/main.rs:11] factorial(4) = 24 "

---

https://ucsd-progsys.github.io/liquidhaskell-blog/

---

The D language uses 'in' and 'out' blocks of code for function preconditions and postconditions: [6]. You put assertions in them. They also have 'invariant' blocks associated with classes which are run (i) after the construction, and (ii) before a destructor (iii) before and after every public member function (including exported library functions) [7].

---

Unknoob on May 18, 2017 [-]

The article is missing the similarities between Swift Optionals and Kotlin Nullables.

Swift code:

  var name: String?
  name? ///returns a safe value
  name! ///returns an unsafe value and throws an exception in case of a nil value
  if name != nil {
   ///Now we can use name! forcing unwrap because we know it's not nil
  }
  if let unwrapedName = name {
   /// Here we can use unwrapedName without forcing the unwrap
  }
 The null checks are almost the same in Kotlin: https://kotlinlang.org/docs/reference/null-safety.html

jorgemf on May 18, 2017 [-]

kotlin make it a bit simpler, once you do name != null you can use it as it is never null after than, there is not need to use let. It makes it more readable.

flaie on May 18, 2017 [-]

In kotlin you can also write:

    var name: String?
    // some code
    name?.let {
        // here name is not nil and can be accessed via variable "it"
    }
    // or 
    name?.let { name ->
        // here name shadows the previous variable, and is not nil
    }

---

static_assert

---

DonHopkins? 4 days ago [–]

Reminds me how the TRS-80 Level 1 Model 1 BASIC interpreter had just three error messages:

http://www.trs-80.org/level-1-basic/

Error Messages

Level I BASIC had three error messages: HOW?, WHAT?, and SORRY. User’s Manual for Level I described the error messages this way:

In general, a HOW? message means, “I understand your instructions, but they’re asking me to do something that’s impossible.”

The WHAT? error message, on the other hand, means, “I don’t understand your instructions — either the grammar is wrong or you’re using words that aren’t in my vocabulary.”

The third and final error message is SORRY. It means “Sorry — you have run out of memory locations and must either cut down the program size or purchase additional memory.”

reply

---

ways in which RAII is better than defer:

Niten on July 3, 2015

parent favorite on: Things Rust shipped without

> The addition of some orderly construct for this in C would eliminate that case, leaving no real role for goto there either.

I like the way this is handled in Go with the defer keyword: https://blog.golang.org/defer-panic-and-recover

This construct gives you most of the power of C++ RAII without the overhead. Except that you can't use it to cleanup resources after exiting an anonymous block—it strictly defers to function exit.

danieldk on July 4, 2015 [–]

This construct gives you most of the power of C++ RAII without the overhead.

But it leaves out the most useful part: in C++ releasing the resources is completely implicit:

    {
        std::ifstream in(fn);
        // Do something with 'in'
    } // Released

while with Go defer, you have to call explicitly for the method that you want to call. If you forget this, you still have a resource leak.

Sure, it's an improvement over languages where you have to cover every possible scope exit, but it definitely doesn't give you 'most of the power' of C++ RAII.

pcwalton on July 3, 2015 [–]

defer has unavoidable runtime overhead due to its dynamic semantics. It's strictly slower than RAII as implemented in C++ or Rust.

giovannibajo1 on July 3, 2015 [–]

That's not true. The only case where the runtime overhead is unavoidable is calling defer in a loop, because it might require allocating the defer chain in the heap; in all other cases, it's just a metter of writing the correct optimization passes in the compiler.

pcwalton on July 4, 2015 [–]

Yes, that's what I meant by "strictly slower". At best, it can be optimized to something similar to what RAII can give you. RAII never has the overhead of the bad case.

giovannibajo1 on July 4, 2015 [–]

I don't want to be excessively picky, but you said that is has "unavoidable runtime overhead", and that's true only in its rarest form (defer within a loop), which is a feature which you can't implement in RAII. IOW, defer is a superset of RAII.

In all other cases (which is almost all usages), it is semantically equivalent to RAII, so the language doesn't force any runtime overhead. The only difference is that the compiler is less mature than an average C++ compiler, but this is an implementation problem, not a design problem.

RAII has no overhead because it is a pattern designed within the context of a zero-overhead language. defer allows you to implement a superset of cases that RAII handles, including those with runtime overhead.

pcwalton on July 4, 2015 [–]

defer isn't a superset of RAII. defer is function-scoped. RAII is block-scoped. If you want to run code at the end of a block, you can do that with RAII and you can't do that with defer—and note that this is what causes all the codegen issues with defer.

Niten on July 4, 2015 [–]

To clarify, I meant the "overhead" of defining and instantiating a container class for RAII purposes.

I hadn't considered the type of overhead you're talking about, which is admittedly more important for the kind of software that tends to get written in C.

pcwalton on July 4, 2015 [–]

There was such a thing (std::finally), but it was removed due to lack of use and maintenance. When your standard library is completely built on RAII, you rarely need it.

-- https://news.ycombinator.com/item?id=9828186

---

willvarfar on June 4, 2014 [–]

(Mill team)

With apologies to those tired of hearing about the new Mill CPU let me explain how the Mill traps on integer overflow:

For overflow-able arithmetic operations, we support four modes:

With excepting overflow, the result is marked as invalid (we term it "Not-a-Result (NaR?)").

As these invalid results are used in further computation, the NaR? propagates.

When you finally use the result in non-speculatively e.g. store it or branch on it then the hardware faults.

NaRs? have lots of other uses.

This is described in the Metadata talk http://millcomputing.com/topic/metadata/

And http://millcomputing.com/topic/introduction-to-the-mill-cpu-... for a broader overview.

---

on a proposal to make 'a smaller Rust' [8] :

16 kornel edited 1 year ago

link

I would not give up Result. To me there’s nothing “systems” about it. It bridges the two worlds of exceptions and return values.

Typically exceptions are tied to the call graph, and can be handled only immediately, in their own scope. OTOH values are independent of the scope in which they were created, and can be handled anywhere at any time, moved, aggregated, copied, stored. Result gives that flexibility to error handling.

It does make the language smaller, because there only needs to be one way to return from a function, not two.

    5
    ac 1 year ago | link | | 

In languages with exceptions I always have trouble deciding when to use exceptions or return values, I really like rust’s way of doing things, it is really clear and the ? operator works well imo.

4 Sophistifunk 1 year ago

link

Zig’s error strategy seems nice, there’s some details in here https://andrewkelley.me/post/intro-to-zig.html but the best description I’ve seen of it is in here https://www.youtube.com/watch?v=Gv2I7qTux7g

1 nerosnm 1 year ago

link

I agree. In Swift (which has been in some ways inspired pretty heavily by Rust), some APIs use its exception system, some use Swift’s equivalent of Option, and now I believe a Result type has been introduced - not to mention having to deal with legacy Objective-C APIs with their own ways of signalling results.

This is partly down to the fact that the APIs are inconsistent, which is fixable, but in comparison to Rust’s Result, I just find this situation so annoying. It bothers me every time I have to call a method that uses exceptions. I wish Swift had a consistent usage of a result type in the same way, and I don’t think it would feel particularly more low-level or “systems” in any way.

---

my take on the previous: yes i agree, Oot should standardize on Result (not Option). Since the beginning i think i wanted Oot to have something like Rust's Result (although i probably would have called it Haskell's Either)

---

we want runtime assertions, and then we also want assertions that are guaranteed to be proven at compiletime (mb only in strict mode) (dafny style i guess?)

---

" Syslog came with the idea of severity levels, which is now defined in the Syslog standard. Syslog comes with the following severity levels:

    Emergency
    Alert
    Critical
    Error
    Warning
    Notice
    Informational
    Debug... In most logging frameworks you will encounter all or some of the following log levels:
    TRACE
    DEBUG
    INFO
    WARN
    ERROR
    FATAL" -- https://sematext.com/blog/logging-levels/

remember that you shouldn't have things sending alerts if they can be ignored:

https://lobste.rs/s/jixmsg/case_against_low_priority_alerts

---

" Enter David Tolnay (again!) and his handy anyhow! crate, which pulls together best practices and ties that into the improvements in the std::error::Error trait to yield a crate that is powerful without being imposing. Now, when an error emerges from within a stack of software, we can get a crisp chain of causality, e.g.:

readmem failed: A core architecture specific error occurred

Caused by: 0: Failed to read register CSW at address 0x00000000 1: Didn't receive any answer during batch processing: [Read(AccessPort?(0), 0)]

And we can set RUST_BACKTRACE to get a full backtrace where an error actually originates — which is especially useful when a failure emerges from a surprising place, like this one from a Drop implementation in probe-rs:

Stack backtrace: 0: probe_rs::probe::daplink::DAPLink::process_batch 1: probe_rs::probe::daplink::DAPLink::batch_add 2: ::read_register 3: probe_rs::architecture::arm::communication_interface::ArmCommunicationInterface::read_ap_register 4: probe_rs::architecture::arm::memory::adi_v5_memory_interface::ADIMemoryInterface::read_word_32 5: <probe_rs::architecture::arm::memory::adi_v5_memory_interface::ADIMemoryInterface? as probe_rs::memory::MemoryInterface?>::read_word_32 6: ::get_available_breakpoint_units 7: <core::iter::adapters::ResultShunt? as core::iter::traits::iterator::Iterator>::next 8: <alloc::vec::Vec as alloc::vec::SpecFromIter?>::from_iter 9: ::drop 10: core::ptr::drop_in_place 11: main 12: std::sys_common::backtrace::__rust_begin_short_backtrace 13: std::rt::lang_start::[[image:closure?]] 14: core::ops::function::impls::<impl core::ops::function::FnOnce?<A> for &F>::call_once 15: main 16: __libc_start_main 17: _start) })

" -- [9]

---

on the Mill CPU:

"you can also push the special values NONE and NAR (Not A Result, similar to NaN?) onto the belt l, which will either NOP out all operations with it (NONE) or fault on nonspeculative operation (i.e. branch condition, store) with it (NAR)."

---

"Zig's errors also carry a trace of each function that they passed through, even if the error changed along the way. This is not the same as the stacktrace at the point the error was created - it's tracking the errors path through your error handling code." -- [10]

---

"Zig prints stacktraces on segfaults!" -- [11]

---

" The nullsafe operator

If you're familiar with the null coalescing operator you're already familiar with its shortcomings: it doesn't work on method calls. Instead you need intermediate checks, or rely on optional helpers provided by some frameworks:

$startDate = $booking->getStartDate();

$dateAsString = $startDate ? $startDate->asDateTimeString() : null;

PHP 7.4

With the addition of the nullsafe operator, we can now have null coalescing-like behaviour on methods!

$dateAsString = $booking->getStartDate()?->asDateTimeString(); "

" Throw expressions

Before PHP 8, you couldn't use throw in an expression, meaning you'd have to do explicit checks like so:

public function (array $input): void { if (! isset($input['bar'])) { throw BarIsMissing::new(); }

    $bar = $input['bar'];
    // …}

PHP 7.4

In PHP 8, throw has become an expression, meaning you can use it like so:

public function (array $input): void { $bar = $input['bar'] ?? throw BarIsMissing::new();

    // …} "

---

" Zig copies the defer concept from Go. But in addition to defer it has errdefer. If you don't know Go, then defer is basically a way to defer the execution of a line or block of code until the function exits.

errdefer...it only gets executed in case an error code is returned. " [12]

eg if an inner function allocates something and returns it to the caller, then you can have the caller deallocate in the 'happy path' but if there is an error then the inner function can deallocate before returning nothing:

" const digits = try decimals(allocator, 4123); defer digits.deinit(); for (digits.items)

digit{
    try print("{},", .{digit});}

fn decimals(alloc: *Allocator, n: u32) !Array(u32) { var x = n; var digits = Array(u32).init(alloc); errdefer digits.deinit();

    while (x >= 10) {
        try digits.append(x % 10);
        x = x / 10;
    }
    try digits.append(x);
    return digits;} "

---

" I would provide syntactic sugar for Result and Option as the way to handle null and errors, similar to Swift. " -- https://without.boats/blog/revisiting-a-smaller-rust/

---

Kotlin 3 kornel edited 2 months ago

link
on: What is your take on checking return values?

Author carefully tries not to point fingers at the language, but “you should check all error conditions

but I don’t want to!” is a very old problem, and different languages have tried to solve it in various ways. I think we’ve had a meaningful progress since ALGOL 68 in this area:
    Making handling less messy with less burdensome syntax for error propagation (like ? in Rust)
    Propagating missing values instead of blowing up (like ?. in Kotlin/TypeScript)
    Making checks mandatory with optional types. If the program blows up, it’s because you wrote force-unwrapping call that makes it so.
    Removing null entirely. Things can only be missing if they’ve been explicitly designed to be optional. That makes “but it was supposed to be there!” less of an excuse.

I haven’t seen a solution that fixes the problem entirely, but in my experience these features can reduce it from a common problem that can happen by accident, to exceptional cases which need either pretty complex data flow and/or code that is cutting corners in ways that are easy to notice in code reviews.

---

"

Error Handling. When it comes to null safety, Kotlin and Rust are mostly equivalent in practice. There are some finer distinctions here between union types vs sum types, but they are irrelevant in real code in my experience. Syntactically, Kotlin’s take on ? and ?: feels a little more convenient more often.

However, when it comes to error handling (Result<T, E> rather than Option<T>), Rust wins hands down. Having ? annotating error paths on the call site is very valuable. Encoding errors in function’s return type, in a way which works with high-order functions, makes for robust code. I dread calling external processes in Kotlin and Python, because it is exactly the place where exceptions are common, and where I forget to handle at least one case every single time. "

---

(this complaint is old, maybe it has been fixed in Golang since, but i'm just writing it here as something to watch out for in Oot)

" Error Handling in Go

The standard Go approach to operations which may fail involves returning multiple values (not a tuple; Go has no tuples) where the last value is of type error, which is an interface whose nil value means “no error occurred.”

Because this is a convention, it is not representable in Go's type system. There is no generalized type representing the result of a fallible operation, over which one can write useful combining functions. Furthermore, it's not rigidly adhered to: nothing other than good sense stops a programmer from returning an error in some other position, such as in the middle of a sequence of return values, or at the start - so code generation approaches to handling errors are also fraught with problems.

It is not possible, in Go, to compose fallible operations in any way less verbose than some variation on

    a, err := fallibleOperationA()
    if err != nil {
        return nil, err
    }
    b, err := fallibleOperationB(a)
    if err != nil {
        return nil, err
    }
    return b, nil

In other languages, this can variously be expressed as

    a = fallibleOperationA()
    b = fallibleOperationB(a)
    return b

in languages with exceptions, or as

    return fallibleOperationA()
        .then(a => fallibleOperationB(a))
        .result()

in languages with abstractions that can operate over values with cases.

"

---

on PIPEFAIL:

https://utcc.utoronto.ca/~cks/space/blog/unix/ShellPipesTwoUsages

---

"needs a mechanism to recover gracefully from OOM errors."

---

"To this list then I would also want to add and compare: OOM-safety under overload conditions, and fine-grained error handling safety, in particular because error handling tends to be one of the leading causes of faults in distributed systems [1]."

https://www.eecg.utoronto.ca/~yuan/papers/failure_analysis_osdi14.pdf

---

" Rust's ownership rules are as follows:

    Each value in Rust has a variable that's called its owner.
    There can only be one owner at a time.
    When the owner goes out of scope, the value will be dropped / released.

Then there are rules about references (think intelligent pointers) to owned values:

    At any given time, you can have either one mutable reference or any number of immutable references.
    References must always be valid.

Put together, these rules say:

    There is only a single canonical owner of any given value at any given time. The owner automatically releases/frees the value when it is no longer needed (just like a garbage collected language does when the reference count goes to 0).
    If there are references to an owned value, that reference must be valid (the owned value hasn't been dropped/released) and you can only have either multiple readers or a single writer (not e.g. a reader and a writer).

The implications of these rules on the behavior of Rust code are significant:

    Use after free isn't something you have to worry about because references can't point to dropped/released values.
    Buffer underruns, overflows, and other illegal memory access can't exist because references must be valid and point to an owned value / memory range.
    Memory level data races are prevented because the single writer or multiple readers rule prevents concurrent reading and writing. (An assertion here is any guards - like locks and mutexes - have appropriate barriers/fences in place to ensure correct behavior in multi-threaded contexts. The ones in the standard library should.)

" -- [13]

---

"Running out of memory simply MUST NOT cause an abort. It needs to just result in an error return."

[14]

---

https://softwareengineering.stackexchange.com/questions/170694/why-error-codes-are-negated

Very often I see in C code negation of returned error codes, e.g. return -EINVAL instead of return EINVAL. Why used negation?

On many UNIXen, convention for syscalls is that they return the error code negated in case of an error, and the actual value in case of success.

This is translated on the user space side into -1 for errors with the positive error code in errno, or the actual value in case of success.

The need for this dual-purposing of the return value is because multiple-returns in this kind of language is bothersome, as you'd have to resort to passing in out-parameters to hold an eventual error.

---

Animats on Feb 28, 2020 [–]

And the other one is the tendency for Go's design to say "exceptions are allowed for me but not for thee".

Yes. Exceptions are kind of a pain, but the workarounds for not having them are worse. Passing back "result" types tends to lose the details of the problem before they are handled. Rust is on, what, their third error handling framework?

Exceptions have a bad reputation because C++ and Java botched them. You need an exception hierarchy, where you can catch exception types near the tree root and get all the children of that exception type. Otherwise, knowing exactly what exceptions can be raised in the stack becomes a huge headache. Python comes close to getting this right.

Incidentally, the "with" clause in Python is one of the few constructs which can unwind a nested exception properly. Resource Acquisition Is Initialization is fine; it's Resource Deletion Is Cleanup that has problems. Raising an exception in a destructor is not happy-making. It's easier in garbage-collected languages, which, of course, Go is. You have to be more careful about unwinding in non garbage collected languages, which was the usual problem in C++.

patrec on Feb 29, 2020 [–]

Internal (bug, e.g. divide by zero or missing function definition in a dynamic language) vs external (out-of-your-control corner case, like file not found) is an important axis.

But another axis is "finality":

1. do you just want to never crash (return code)

2. sometimes crash but have the ability to deal with the problem up the callchain (exceptions in most languages)

3. sometimes crash but be able to fix the problem and continue, at the point the error occured -- not up the call stack (restart-case etc. in common lisp)

4. sometimes crash, but have a supervisor hierarchy make an informed decision if and how to restart you and things in your dependency tree (erlang)

5. crash (panic, assert, exit) and maybe have some less sophisticated but probably very complicated mechanism take care of restarting/replacing you (systemd, kubernetes etc.)

This axis may not be completely orthogonal, but probably mostly is. For example resumable conditions are nice in common lisp both to deal with external stuff (no space left on device? ask user to abort or free some up, and just resume download instead of erroring out as webbrowsers do) but also to just fix problems as you run into them and continue your computation during development, including calling a function you did not define – you can just define it and resume the call to it.

Sadly, the choices in most languages for this second axis are much more constrained. Erlang's supervision trees and common lisp's resumable exceptions in particular seem very useful in many scenarios but nothing else has them (well, elixir has everything erlang has, but it's still the same VM/ecosystem).

Measter on Feb 29, 2020 [–]

>Yes. Exceptions are kind of a pain, but the workarounds for not having them are worse. Passing back "result" types tends to lose the details of the problem before they are handled. Rust is on, what, their third error handling framework?

Rust's non-panicking error handling hasn't really changed: you return a Result<SuccessType?, ErrorType?>.

What has changed is the details of how to implement your ErrorType?. Should it store some sort of context? What useful helper functions can there be? Things like that. What these error handling frameworks provide is macro-based code generation to implement these details, and extension traits for the helper funcitons. They don't change overall method of error handling.

Or, at least, I've not seen one that does.

---

    8
    mbrock 1 year ago | link | 

However, sometimes it is quite annoying to conflate the type and structure of the list with the fact that it is nonempty. For example the functions from Data.List don’t work with the NonEmpty? type. I think the paper on “departed proofs” linked near the bottom points to a different approach where various claims about a value are represented as separate proof objects.

    3
    endgame 1 year ago | link | 

In this instance, the fact that both [] (lists) and NonEmpty? are instances of Foldable will help: http://hackage.haskell.org/package/base-4.12.0.0/docs/Data-Foldable.html

---

 chmln on Jan 6, 2019 [–]

This went past me as the post is filled with a lot of claims with no reasoning to back those up. It is not a critical evaluation of the language, but rather sounds like a "fanboy" piece, for the lack of a better term.

> Memory efficiency is much better than most other languages (with the exception of Rust, but Elixir is miles better at Error handling than Rust, which is a more practical feature IMO

How exactly are arbitrary runtime exceptions better? Any elixir function you call has the potential to crash. Meanwhile with Rust, your function returns a `Result` if it can error, and callers are then forced to handle those by the compiler, either via pattern matching or ergonomic error propagation.

Rust has runtime panics, but those are for rare unrecoverable errors and are not at all used for conventional error handling, reserved usually for C FFI, graphics code, etc.

josevalim on Jan 6, 2019 [–]

I am not sure I would say one is better than the other, but they are very different.

As you said, in Rust you are forced to handle errors by the compiler. In Elixir, you actually don't. In fact, we even encourage you to [write assertive code](http://blog.plataformatec.com.br/2014/09/writing-assertive-c...). This is also commonly referred as "let it crash". In a nutshell, if there is an unexpected scenario in your code, you let it crash and let that part of the system restart itself.

This works because we write code in tiny isolated processes, in a way that, if one of those processes crash, they won't affect other parts of the system. This means you are encouraged to crash and let supervisors restart the failed processes back. I have written more about this in another comment: https://news.ycombinator.com/item?id=18840401

I also think looking at Erlang's history can be really interesting and educational. The Erlang VM was designed to build concurrent, distributed, fault-tolerant systems. When designing the system, the only certainty is that there would be failures (hello network!) so instead trying to catch all failures upfront, they decided to focus on a system that can self-heal.

I personally think that Erlang and Elixir could benefit from static types. However, this is much easier said than done. The systems built with those languages tend to be very dynamic, by even providing things such as hot code swapping, and only somewhat recently we have started to really explore the concepts required to type processes. But a more humble type system could start with the functional parts of the language, especially because I think that other techniques of model checking can be more interesting than type systems for the process part.

---

rwmj on April 14, 2020 [–]

Not a question, a request: Please make __attribute__((cleanup)) or the equivalent feature part of the next C standard.

It's used by a lot of current software in Linux, notably systemd and glib2. It solves a major headache with C error handling elegantly. Most compilers already support it internally (since it's required by C++). It has predictable effects, and no impact on performance when not used. It cannot be implemented without help from the compiler.

rseacord on April 14, 2020 [–]

My idea was to add something like the GoLang? defer statement to C (as a function with some special compiler magic). The following is an example of how such a function could be used to cleanup allocated resources regardless of how a function returned:

  int do_something(void) {
    FILE *file1, *file2;
    object_t *obj;
    file1 = fopen("a_file", "w");
    if (file1 == NULL) {
      return -1;
    }
    defer(fclose, file1);
  
    file2 = fopen("another_file", "w");
    if (file2 == NULL) {
      return -1;
    }
    defer(fclose, file2);
    obj = malloc(sizeof(object_t));
    if (obj == NULL) {
      return -1;
    }
    // Operate on allocated resources
    // Clean up everything
    free(obj);  // this could be deferred too, I suppose, for symmetry 
  
    return 0;
  }

rwmj on April 14, 2020 [–]

Golang gets this wrong. It should be scope-level not function-level (or perhaps there should be two different types, but I have never personally had a need for a function-level cleanup).

Edit: Also please review how attribute cleanup is used by existing C code before jumping into proposals. If something is added to C2x which is inconsistent with what existing code is already doing widely, then it's no help to anyone.

rseacord on April 14, 2020 [–]

Yes, we have discussed adding this feature at scope level. A not entirely serious proposal was to implement it as follows:

  1. define DEFER(a, b, c) \ for (bool _flag = true; _flag; _flag = false) \ for (a; _flag && (b); c, _flag = false)
  int fun() {
     DEFER(FILE *f1 = fopen(...), (NULL != f1), mfclose(f1)) {
       DEFER(FILE *f2 = fopen(...), (NULL != f2), mfclose(f2)) {
         DEFER(FILE *f3 = fopen(...), (NULL != f3), mfclose(f3)) {
             ... do something ...
         }
       }
     }
  }

We are also looking at the attribute cleanup. Sounds like you should be involved in developing this proposal?

a1369209993 on April 15, 2020 [–]

Apropos of this, I'll toss in: please support do-after statements (and also let statements).

  do foo(); _After bar();
  /* exactly equivalent to (with gcc ({})s): */
  ({ bar(); foo(); });
  #define DEFER(a, b, c) \
    _Let(a) if(!b) {} else do {c;} _After

(This is in fact a entirely serious proposal, though I don't actually expect it to happen.)

rwmj on April 14, 2020 [–]

Yes, I'll ask around in Red Hat too, see if we can get some help with this.

nrclark on April 15, 2020 [–]

Would it make sense for defer to operate on a scope-block, sort of like an if/do/while/for block instead?

That would allow us to write:

   defer close(file);

or:

   defer {
      release_hardware();
      close(port);
   }

I feel like that syntax fits very nicely with other parts of C, and could even potentially lend itself well to some very subtle/creative uses.

I feel like a very C-like defer would:

eqvinox on April 14, 2020 [–]

Cleanup on function return is not enough, it needs to be scope exit. We're using this for privilege raising/dropping (example posted above) and also mutex acquisition/release. Both of these really "want" it on the scope level.

jart on April 15, 2020 [–]

Go-like defer() is easily implementable for C using the asm() keyword. Here's an example of how it can be done for x86: https://gist.github.com/jart/aed0fd7a7fa68385d19e76a63db687f...

rwmj on April 14, 2020 [–]

We have a nice macro for acquiring locks that only applies to the scope:

https://github.com/libguestfs/nbdkit/blob/e58d28d65bfea3af36...

You end up with code like this:

https://github.com/libguestfs/nbdkit/blob/e58d28d65bfea3af36...

It's so useful to be able to be sure the lock is released on all return paths. Also because it's scope-level you can scope your locks tightly to where they are needed.

---

rseacord on April 14, 2020 [–]

So what do people think about having a feature in the C language akin to the defer statement in GoLang??

The GoLang? defer statement defers the execution of a function until the surrounding function returns. The deferred call's arguments are evaluated immediately, but the function call is not executed until the surrounding function returns. It looks like an interesting mechanism for cleaning up resources.

pascal_cuoq on April 14, 2020 [–]

It sounds like the __attribute__((cleanup(…))) already offered by GCC is similar to this. I probably won't have time to investigate the differences while the AMA is ongoing though.

---

in Rust, sounds like the current recommendation for error handing is to use one of these two libraries: anyhow, thiserr:

d1plo1d on April 9, 2020 [–]

As someone newer to Rust who has been using error-chain because it was what I found at the time I'd be curious to hear what your preferred solution to errors in modern rust is.

steveklabnik on April 9, 2020 [–]

https://blog.yoshuawuyts.com/error-handling-survey/ is a good summary of what's out there.

I think current rough consensus is anyhow/thiserr depending on if you're writing an application or library. I haven't actually used them myself, though. You don't have to keep up with the cool new libraries.

matklad on April 9, 2020 [–]

Agree that there’s consensus on anyhow for applications, but I think many folks now prefer vanilla std::error::Error for libraries (which itself got better as a result of all the experiments in the ecosystem).

littlestymaar on April 9, 2020 [–]

The good thing with this_error is that it's just a custom derive on vanilla std::error::Error.

ShorsHammer? on April 9, 2020 [–]

Failure is really nice to use.

Might suit some people.

https://github.com/rust-lang-nursery/failure

bschwindHN on April 9, 2020 [–]

I wouldn't recommend failure, its current maintainer and original author will be deprecating it:

https://boats.gitlab.io/blog/post/failure-to-fehler/

From the article:

> The crate I would recommend to anyone who likes failure’s API is anyhow, which basically provides the failure::Error type (a fancy trait object), but based on the std Error trait instead of on.

---

"OK wrapping" proposal in Rust (avoid the need to type Ok(...) around everything in the happy path):

" We would add a syntactic modifier to the signature of a function, like this:

fn foo() -> usize throws io::Error { .. }

This function returns Result<usize, io::Error>, but internally the return expressions return a value of type usize, not the Result type. They are “Ok-wrapped” into being Ok(usize) automatically by the language. If users wish to throw an error, a new throw expression is added which takes the error side (the type after throws in the signature). The ? operator would behave in this context the same way it behaves in a function that returns Result.

" [15]

---

summary from https://blog.yoshuawuyts.com/error-handling-survey/ on Rust error handing improvement libraries and what the 'rough consensus' on needed features is:

" There seems to be rough consensus in the ecosystem that we seem to need:

    Some kind of replacement for Box<dyn Error + Send + Sync + 'static>
    Some way of wrapping Results in .context.
    Some way to conveniently define new error types.
    Some way to iterate over error causes (#58520).
    Support for backtraces (#53487).

Additionally the functionality provided by ensure!, bail!, and format_err! has been exported as part of many of the libraries, and seems to be along the lines of: "stable, popular, and small" that is the bread and butter of std. "

---

Animats on Feb 28, 2020 [–]

> And the other one is the tendency for Go's design to say "exceptions are allowed for me but not for thee".

Yes. Exceptions are kind of a pain, but the workarounds for not having them are worse. Passing back "result" types tends to lose the details of the problem before they are handled. Rust is on, what, their third error handling framework?

Exceptions have a bad reputation because C++ and Java botched them. You need an exception hierarchy, where you can catch exception types near the tree root and get all the children of that exception type. Otherwise, knowing exactly what exceptions can be raised in the stack becomes a huge headache. Python comes close to getting this right.

Incidentally, the "with" clause in Python is one of the few constructs which can unwind a nested exception properly. Resource Acquisition Is Initialization is fine; it's Resource Deletion Is Cleanup that has problems. Raising an exception in a destructor is not happy-making. It's easier in garbage-collected languages, which, of course, Go is. You have to be more careful about unwinding in non garbage collected languages, which was the usual problem in C++.

---

in the context of replying to https://fasterthanli.me/blog/2020/i-want-off-mr-golangs-wild-ride/

gameswithgo on Feb 28, 2020 [–]

A lot of people seem to be missing an overarching point, which is the benefits of a language having Sum types, so that edge cases can be represented clearly, and in a way where the consumer of the api can't fail to know they exist, and can't fail to handle them. Anyone thinking of making a new language today, should really get some familiarity with Option and Result types. They make so many things not only safer, but also nicer to use. [https://news.ycombinator.com/item?id=22443363 ]

---

on checked exceptions vs results:

" masklinn on Feb 28, 2020 [–]

Checked exceptions were universally rejected not because they are intrinsically bad but because the language support was awful (e.g. could not wrap or abstract over a nested object possibly rethrowing), they were sitting right next to unchecked exception with limited clarity, guidance and coherence as to which was which, and they are so god damn ungodly verbose, both to (re)throw and to convert.

Results are so much more convenient it's not even funny, but even without that you could probably build a language with checked exceptions where they're not infuriatingly bad (Swift has something along those lines, though IIRC it doesn't statically check all the error types potentially bubbling up so you know that you have to catch something, not necessarily what).

Groxx on Feb 29, 2020 [–]

A very large part of that though is Java not being 'generic' over checked exception types. So if you e.g. build something that supports end-user callback code, you need to either throw Exception (accepting all code but losing all signal as to what's possible) or nothing (forcing RuntimeException? boxing).

That's Java. And I agree it is a wildly painful and incomplete implementation. I wish we'd stop conflating it with checked exceptions as a language feature.

" -- [16]

akavi on Feb 28, 2020 [–]

Result types are genericizable in a way that checked exceptions aren't (IIRC), which is huge for ergonomics.

AnimalMuppet? on Feb 28, 2020 [–]

Can you give an example of how you think this helps?

masklinn on Feb 29, 2020 [–]

Basically, exceptions have a "happy path" which is very simple but deviating from that path is often quite inconvenient and painful. A well-built result type makes it easy to opt into the happy path of exceptions, and also quite easy to use different schemes and deviate from that path, all the while being much safer than exceptions because you're not relying on runtime type informations and assumptions.

Furthermore, results make it much less likely to "overscope" error handlers (there a try block catches unrelated exceptions from 3 different calls) as the overhead is relatively low and there's necessarily a 1:1 correspondance between calls and results; and it's also less likely to "miscatch" exceptions (e.g. have too broad or too narrow catch clauses) because you should know exactly what the call can fail with at runtime. It's still possible to make mistakes, don't get me wrong, but I think it's easier to get things right.

"Path unification" is a big one in my experience: by design exceptions completely split the path of "success" and "failure" (the biggest split being when you do nothing at all where they immediately return from the enclosing function).

This is by far the most common thing you want so in a way it makes sense as a default, but it's problematic when you don't want the default because then things get way worse e.g. if you have two functions which return a value and can fail and you need to call them both, now you need some sort of sentinel garbage for the result you don't get, and you need a bunch of shenanigans to get all the crap you need out

    int a;
    SomeException e_a = null;
    try {
        a = something();
    } except (SomeException e) {
        a = -1;
        e_a = e;
    }
    int b;
    SomeException e_b = null;
    try {
        b = something();
    } except (SomeException e) {
        b = -1;
        e_b = e;
    }
    if (e_a != null or e_b != null) { // don't mess that up because both a and b are "valid" here
        …
    }

or you duplicate the path in both the rest of the body and the except clause (possibly creating a function to hold that), etc…

By comparison, results are a reification so splitting the path is an explicit operation, but at the same time they still don't allow accessing the success in case of failure, or the failure in case of success.

    let result_a = something();
    let result_b = something();
    if let Err(_) = result_a.and(result_b) { // or pattern matching or something else
        …
    }

Having a reified object also allows building abstractions on top of it much more easily e.g. if you call a library and you want to convert its exceptions into yours you need to remember to

    try {
        externalCall()
    } except (LibraryException e} {
        throw MyException.from(e); // because that might want to dispatch between various sub-types
    }

and if you don't remember to put this everywhere the inner exception will leak out (that's assuming you don't have checked exceptions because Java's are terrible and nobody else has them).

Meanwhile with results the Result from `externalCall` is not compatible with yours so this:

    return externalCall();

will fail to compile with a type mismatch, and then you can add convenience utilities to make it easy to convert between the errors of the external library and your own, and further make it easy to opt into an exception-style pattern. e.g. Rust's `?`

    externalCall()?

is roughly:

    match externalCall() {
        Ok(value) => value,
        Err(error) => { return Err(From::from(error))); }
    }

(there's actually more that's involved into it these days an a second intermediate trait but you get the point, in case of success it just returns the success value and in case of failure it converts the failure value into whatever the enclosing function expects then directly returns from said enclosing function).

---

" eturn values.

I want to live in a world where the function looks like this:

func myFunc() (int, string, error) { foo()? bar()? return baz()? }

Here the question mark tells us that if the function returns an error, we should return that error with zero-values for all the other return values. Otherwise, we return the non-error values from the function. " -- [17]

---

jamespwilliams 1 day ago [–]

Maybe it’s just Stockholm Syndrome, but IMO Go’s “verbose” error handling approach is a feature, not a bug. I think the expectation that every single error is handled and (importantly) wrapped explicitly is one of the reasons why Go and the Go ecosystem in general is conducive to writing robust and resilient systems. When writing Go, error handling is at the forefront of my mind, and not an afterthought, as it can be in other languages.

reply

nicoburns 1 day ago [–]

Have you used Rust? Because it has the same explicit error handling, except it has syntax sugar (the '?' operator) which makes it painless. Additionally it's robustness is even better than Go's, as the Result type it uses for errors makes it impossible (compile time error) to access the return value without first checking for an error.

reply

chrismorgan 1 day ago [–]

For comparison:

  fn my_func() -> Result<(), Error> {
      foo()?;
      bar()?;
      baz()?;
      Ok(())
  }
  // ---
  let val = bar()?;
  println!("{}", val);
  // ---
  fn my_func() -> Result<i32, Error> {
      foo()?;
      bar()?;
      let val = baz()?;
      // ... more stuff
      Ok(val)
  }
  // ---
  fn my_func() -> Result<(i32, String), Error> {
      foo()?;
      bar()?;
      let (val1, val2) = baz()?;
      Ok((val1, val2))
      // Those last two lines could even be just `baz()` or `Ok(baz()?)`.
  }

Rather close to the world the author wants to live in.

reply

thiht 1 day ago [–]

What happens if you want to log something before returning the error, or wrap the error, add some context or something ? Is there a way to override the ? operator? Or do you fallback to a matcher, which is roughly the same as Go error handling?

reply

nicoburns 1 day ago [–]

If you want to manually wrap/change the error somehow then the pattern would be something like:

     foo().map_err(|err| WrapperError::new(err))?

For context, typically you would use a library like `anyhow` which allows you to do:

     foo().context("Context string here")?

or

     foo().with_context(|| format!("dyanmically generated context {}", bar))?

I don't know of any pre-built solutions for logging, but you could easily add a method to Result yourself that logged like:

    foo().log("log message")?

or something like:

    foo().log(logger, 'info', "log message")?

The code to do this would be something like:

    use log::error;
    
    trait LogError {
        fn log(self, message: impl Display) -> Self;
    }
    
    impl<T, E> LogError for Result<T, E> {
        fn log(self, message: impl Display) -> Self {
            error!("{}", message);
            self
        }
    }

Then you'd just need to import the `LogError?` error trait at the top of your file to make the `.log()` method available on errors.

reply

samhw 1 day ago [–]

Nice to see so many people mentioning `map_err`. This is exactly what I've done when working with Rust (I have professional experience with both, slightly more with Go) and it's a dream compared with Go's error handling.

reply

politician 1 day ago [–]

What if you want to log, then emit some time series data?

Is chaining+traits+wrappers+map_err truly less complex than a conditional:

  if BAD {
    Log()
    Metric1()
    Metric2()
  }

reply


ways to improve safety:

" Memory safety...Garbage collection...Concurrency...Static analysis ... no false positives (eg RV-Match, Astree), ... false positives (eg PVS-Check, Coverity)...Test generators. Path-based, combinatorial, fuzzing… many methods " -- nickpsecurity

---

" Result and option types

In addition to the standard types available in Rust(boolean, numeric, textual, etc.), there is also an option type. This type can either contain a value or not contain any value (and therefore be none). Result and option types are very commonly used in Rust, but it can be difficult to understand these types and use them effectively.

In V, the process is very simplified. A ? can be used in place of a function’s return type, letting it either return a value, an error, or a none. Thus, V combines option and result type into ?. With this functionality, error handling is also a lot easier to tackle.

Take a look at the use of ? in the sample code below, taken from the V docs:

fn (r Repo) search_user_by_id(id int) ?User { for user in r.users { if user.id == id { V automatically wraps this into an option type return user } } return error('User $id not found') } "

---

verbose info warning error

---

in web browsers:

" What is that you’re logging?

The next problem with using `console.log()` is that we seem to only log values and forget to add where they come from. For example, when you use the following code, you get a list of numbers, but you don’t know what is what.

console.log(width) console.log(height)

The easiest way to work around that issue is to wrap the things you want to log in curly braces. The console then logs both the name and the value of what you want to know about.

console.log({width}) console.log({height}) "

console.trace()

" Grouping console messages

If you have a lot to log, you can use `console.group(‘name’)` and `console.groupEnd(‘name’)` to wrap the messages in collapsible and expandable messages in the Console. You can even define if the groups should be expanded or collapsed by default. " (groupCollapsed to collapse by default)

"The `console.table()` method displays array-like data as a table in the console, and you can filter what you want to display by giving it an array of the properties you want to see.

For example, you can use `let elms = document.querySelectorAll(‘:is(h1,p,script’)` to get all H1, paragraph and script elements from the document and `console.table(elms)` to display this information as a table. As the different elements have a boatload of attributes and properties, the resulting table is pretty unreadable. If you filter down to what you are interested in by using `console.table(elms,[‘nodeName’, ‘innerText’, ‘offsetHeight’])` you get a table with only these properties and their values. "

"Blinging it up: $() and $$()

The console comes with a lot of convenience methods you can use called the Console Utilities . Two very useful ones are `$()` and `$$()` which are replacements for `document.querySelector()` and `document.querySelectorAll()` respectively. These not only return the nodeList you expect, but also cast the results to arrays, which means you can use `map()` and `filter()` on the results directly. The following code would grab all the links of the current document and return an Array with objects that contain only the `href` and `innerText` properties of each link as `url` and `text` properties.

$$('a').map(a => { return {url: a.href, text: a.innerText} }) "

" 2. You can log without source access – live expressions and logpoints (Chromium browsers)

The normal way to add a `console.log()` is to put it inside your code at the place you want to get the information. But you can also get insights into code you can’t access and change. Live expressions are a great way to log information without changing your code.

https://christianheilmann.com/wp-content/uploads/2021/10/log-vs-live-expression-smaller.mp4 "

" Logpoints are a special kind of breakpoint. You can right-click any line in a JavaScript? in the Sources tool of the Developer Tools and set a logpoint. You get asked to provide an expression you’d like to log and will get its value in the console when the line of code is executed. This means you can technically inject a `console.log()` anywhere on the web. I "

" 4. You can inject code into any site – snippets and overrides. (Chromium Browsers)

Snippets are a way in Developer Tools to run a script against the current web site. You can use the Console Utilities in these scripts and it is a great way to write and store complex DOM manipulation scripts you normally execute in the Console. You can run your scripts in the window context of the current document either from the snippets editor or from the command menu. In the latter case, start your command with an ! and type the name of the snippet you want to run.

Overrides allow you to store local copies of remote scripts and override them when the page loads. This is great if you have, for example, a slow build process for your whole application and you want to try something out. It is also a great tool to replace annoying scripts from third party web sites without having to use a browser extension. "

-- [18]

" The biggest "secret" not mentioned here is keyboard navigation. It is incredibly cumbersome to operate "Developer Tools" on a small form factor computer without a mouse. Here is one solution: In Chrome's, F12 to open Developer Tools, then Ctrl-P then type ">". Then can scroll all available commands using Up/Down or start typing and use autocomplete to search for commands. Whether this is faster and more effective than pointing+clicking/tapping on tiny screen areas, ticking/unticking tiny check boxes and scrolling around in tiny menus is a question for the reader. Talk amongst yourselves. For example, to navigate to Overrides, the key sequence is F12,Ctrl-P,">","des",Enter " -- [19]

"

amatecha 6 days ago

prev next [–]

Here's a really obscure one which I've never heard anyone mention, except the first time I heard about it:

In the JS console:

  $0

is a reference to the currently-highlighted element in the DOM Inspector (at least this is the case in Firefox, Chrome and Edge, haven't tried others).

Very handy for quickly evaluating or operating on an element you're looking at :)

reply

jakear 6 days ago

parent next [–]

You'll see that in the Elements panel, it actually puts a grey " == $0" after the selected element to point you towards this feature.

Also, $1 is the previously selected element, $2 is the one before that, etc.

reply "

"

recursivedoubts 6 days ago

prev next [–]

not mentioned in the article, but my favorite console trick is

  monitorEvents(elt)

https://twitter.com/htmx_org/status/1455242575363723265

hugely helpful when debugging event-driven systems like htmx or hyperscript

reply

mkl 5 days ago

parent next [–]

And unmonitorEvents(elt) to turn it off again. Monitoring every event is pretty noisy, so you can choose what to monitor with a second parameter: https://briangrinstead.com/blog/chrome-developer-tools-monit...

It would be nice if this made live expressions or something instead of filling up the console.

reply "

---

"

~ technomancy 15 hours ago

link flag
    how Lua can return null values when a value isn’t present in a collection

This is honestly kind of an odd complaint since it’s also true of nearly every other dynamic language out there. (Racket and Erlang being the main exceptions I know of.) Lua’s behavior (and thus Fennel’s too) is sublty different in that nil is much more consistently used to represent the absence of data, rather than other languages where you can have absurd situations like “I put nothing in this list and now the list is longer than it used to be”. In Fennel/Lua there is no difference between [] and [nil]. This takes some adjusting to if you are used to the other behavior but once you learn it, it’s a lot less error-prone.

Fennel has a few advantages over Lua that make it easier to deal with nils when they do occur. The other comment mentions the nil-safe deep accessor macro ?. but there is also automatic nil-checks for function arguments with lambda as well as the fact that the pattern matching system defaults to including nil-checks in every pattern.

    ~
    agent281 14 hours ago | link | flag | 
    This is honestly kind of an odd complaint since it’s also true of nearly every other dynamic language out there.

They’re primarily working in statically typed languages (C++, C#) so it’s not totally a dig against Lua. Just normal workflow mismatch vs dynamically typed paradigm. Also, it was in the context of video game designers using scripting languages (e.g., Blueprint), getting out of their depth, and losing a couple days on bugs.

    (Racket and Erlang being the main exceptions I know of.)

Python also throws when you try to access a value that isn’t present in a dictionary. Always reminds me of this blog post:

    I liken this to telling each language to go get some milk from the grocery store. Ruby goes to the store, finds no milk and returns no milk (nil). Python goes to the store, finds no milk and BURNS THE STORE TO THE GROUND (raises an exception)!
    In Fennel/Lua there is no difference between [] and [nil]. This takes some adjusting to if you are used to the other behavior but once you learn it, it’s a lot less error-prone.

That is interesting! I didn’t know it did that.

Thanks for taking the time to respond! I don’t really work in the video game space, but fennel seems like an interesting choice for video games. Piggy backing off of Lua for embedding in game engines and using macros to create DSLs seems like an interesting niche.

E.g., I could imagine game designers editing rules in a minikanren DSL to affect changes in the game world.

    ~
    technomancy edited 2 hours ago | link | flag | 

Haha; yeah can’t believe I forgot about Python. I listed Erlang and Racket there because they specifically do not even have a notion of nil/null in the entire language, whereas Python somehow manages to include null in their semantics, but not actually use it for this.

    I liken this to telling each language to go get some milk from the grocery store. Ruby goes to the store, finds no milk and returns no milk (nil). Python goes to the store, finds no milk and BURNS THE STORE TO THE GROUND (raises an exception)!

On the other hand I’m actually somewhat sympathetic to this because nils are the most common type error in my experience by at least an order of magnitude, so I would characterize it differently. To me it’s more like you ask Ruby (or Clojure, or JS, or whatever) to go to the store to get some milk, it comes back, walks over to you, says “here’s what I got you”, holds out its empty hand, and drops nothing into your hand.

If a human did that it would be considered extremely disrespectful or overly sarcastic! Throwing an exception is indeed an overreaction, but there needs to be some middle ground where you don’t just pretend that the operation was successful, and most dynamic languages simply don’t have the ability to convey this. https://blog.janestreet.com/making-something-out-of-nothing-or-why-none-is-better-than-nan-and-null/

edit: the reason this works better in Erlang than other dynamic languages is that pattern matching is baked into the core of the language so it’s easy to represent success as a tuple of [ok, Value] which encourages you to match every time you have an operation which might in other languages return nil. Fennel has a pattern matching system which was in many ways inspired by Erlang.

    ~
    agent281 1 hour ago | link | flag | 
    the reason this works better in Erlang than other dynamic languages is that pattern matching is baked into the core of the language so it’s easy to represent success as a tuple of [ok, Value] which encourages you to match every time you have an operation which might in other languages return nil. Fennel has a pattern matching system which was in many ways inspired by Erlang.

That’s great to hear! I haven’t used Erlang, but I’ve used a bit of Elixir and I really love that design. Jose Valim described assertive code in an article a while back and I appreciate that perspective. That’s another reason why I like python burning down the store so to speak. Having the option to say “hey, I really mean it this time” is always useful.

I once spent a day (or more?) tracking down a bug in a poorly tested Node JS codebase. It turns out that the SQL Server stored procedure was returning ID and the Node process was looking for Id. Of course, JavaScript? will happily work with whatever you give it so it was a pain to find. The exception occured way further down the line. It’s hard not to get little bitter after an experience like that.

"

---

" ... It's also fun to flex tools like TLA+, property based testing tools, fuzzers, etc ... "

--- Oil 0.10.0 - Can Shell's Error Handling Be Fixed Once and For All?

https://www.oilshell.org/blog/2022/05/release-0.10.0.html

---

singularity2001 1 day ago

parent prev next [–]

I think making things syntactically explicit which are core concepts is stupid:

```pub fn horror()->Result{Ok(Result(mut &self))}```

A function returns a Result. This concept in Rust is so ubiquitous that it should be a first class citizen. It should, under all circumstances, be syntactically implicit:

```pub fn better->self```

No matter what it takes to make the compiler smarter.

reply

dragonwriter 1 day ago

root parent next [–]

> A function returns a Result.

That is not, in fact, a core concept in Rust. Plenty of functions have no reason to return Result. (And some that do also have a reason for the inner class to be a result.)

> This concept in Rust is so ubiquitous that it should be a first class citizen. It should, under all circumstances, be syntactically implicit:

“Implicit” is an opposed concept to “first-class citizen”. Result is first-class in Rust, and would not be if function returns were implicitly Result.

reply

singularity2001 1 day ago

root parent next [–]

> Result is not a core concept in Rust.

If you don't see std::result::Result as a core concept in Rust, which might be fair, one can still argue that it _should_ be a core concept, given its ubiquitous usage.

reply

dragonwriter 1 day ago

root parent next [–]

You misquoted, I never said Result is not a core concept.

What I said is that “A function returns Result” in the universal sense (that is, everything that is a function returns Result) is not a core concept in Rust.

Some functions return Result<T,E> for some <T,E>. Some functions return Option<T> for some T. Some functions have no reason to use that kind of generic wrapper type (a pure function that handles any value in its range and returns a valid value in a simple type for each doesn't need either; Option/Result are typically needed with otherwise non-total functions or functions that perform side effects that can fail.)

reply

dllthomas 1 day ago

root parent prev next [–]

Others have addressed the problem with "implicit", but I might be on board with "lightweight"; maybe in a type context `T?` can mean `Result<T>` for whatever Result is in scope? That way you can still define functions with various distinct error types the same as today, but the common (idk just how common, not claiming a majority) case of using the same error across a module or package with a Result type alias will get cleaner.

reply

scns 23 hours ago

root parent next [–]

That would be confusing, because T? means Option<T> in other languages (Kotlin, Typescript).

reply

---

logging

" Level When to use DEBUG For some really repetitive information. It might be useful to understand the whole context of what's going on, most of the time it's not so useful. INFO When something relevant happened, something worthy of being aware of most of the time. WARNING Something weird happened (but didn't interrupt the flow/operation). If any other issue happens later on it might give you a hint. ERROR An error happened, it should be resolved as soon as possible. CRITICAL A very serious error happened, it needs immediate intervention. Prefer ERROR if unsure. ... WARNING is no good reason for stopping a flow, but it's a heads up if any other issue happens later. CRITICAL should be the most alarming log you're ever going to receive, it should be a good excuse to wake you up at 3 am to resolve. " -- https://guicommits.com/how-to-log-in-python-like-a-pro/

---

"What you really want is a way to generically express behaviors of an abstraction in a way that can be automatically tested. I think that Clojure's spec is much closer to what's needed than statically checked interfaces. The idea is that when someone implements an abstraction, they can automatically get tests that their implementation implements the abstraction correctly and fully, including the way it behaves. If you've implemented an AbstractArray?, one of the tests might be that if you index the array with each index value returned by `eachindex(a)` that it works and doesn't produce a bounds error." -- [20]

---

[21] argues that Rust shouldn't have 'unwrap', which takes an Result (which is similar to Haskell's Maybe type except a specific error is included) and if it is an error, panics with a generic error message; instead, Rust's 'expect' does the same thing but with an error message provided at the callsite

---

http://joeduffyblog.com/2016/02/07/the-error-model/

---

https://naiveai.hashnode.dev/rust-result-cool

discussion: https://lobste.rs/s/qtsga5/rust_s_result_type_is_cool

---

~ Sophistifunk 3 hours ago

link flag

I used to be enamoured with the idea of result/option types, but you either need syntax sugar, which means only the builtin result/option types are nice, or an unwrap in your function somewhere. And you can’t weaken arguments or strengthen result types without making a breaking change to your API.

All of these issues go away with first-class unions, you can strengthen your outputs from String

Error to just String without breaking (as much) client code, and you can do the reverse with arguments: doSomething(String) can be updated to doSomething(Stringnull) without breaking any client code.
    ~
    kristof 3 hours ago | link | flag | 
    You could expose the try operator to polymorphism so that everyone can use the syntax if they want to. That’s basically what Haskell does.

~ briankung 8 hours ago

link flag
    You can even use ? on Option values in a function that returns Option, which is a fact I really wish was more highly advertised.

Oh hey, TIL!

---

https://github.com/golang/go/issues/53435

---

https://blog.burntsushi.net/rust-error-handling/ https://www.shuttle.rs/blog/2022/06/30/error-handling https://lobste.rs/s/yrvy14/more_than_you_ve_ever_wanted_know_about https://www.lpalmieri.com/posts/error-handling-rust/

---

Error Handling In Zig https://www.aolium.com/karlseguin/4013ac14-2457-479b-e59b-e603c04673c8 https://lobste.rs/s/pyjwmv/error_handling_zig

---

https://without.boats/blog/why-ok-wrapping/

---