Skip to content

Explicit Error Propagation (Zig-Inspired) #4630

@sanathusk

Description

@sanathusk

Summary

This proposal suggests introducing explicit error handling primitives inspired by Zig and Swift. It aims to move Dart towards safer, checked exception handling without breaking existing unchecked exception flows, using the familiar try keyword as an expression.

1. Motivation

Current Dart exceptions are unchecked; a caller has no syntactic indicator that a function might throw. This leads to hidden runtime failures and "catch-all" debugging. This proposal adds visible fallibility to function signatures and call sites, making control flow explicit.

2. Proposed Syntax

Phase 1: The Fallible Marker (!)

We introduce a syntactic sugar ! to return types.

  • Current: Future<User> fetchUser() (Might throw, might return User).
  • Proposed: Future<User>! fetchUser() (Explicitly returns User OR Error).

The ! acts as a compile-time flag. It forces the caller to acknowledge the potential failure.

Phase 2: Propagation (try Expression)

We overload the try keyword to act as a prefix operator when handling ! types.

Usage Rules:

  1. Propagation (Early Return): When used before a function call, try attempts to unwrap the value. If the function fails, it immediately returns the error to the caller (similar to Zig).
  2. Visual Clarity: It distinguishes "dangerous" calls from standard calls at a glance.

3. Code Examples

A. Defining a Fallible Function

// The '!' indicates this function returns a String or throws an error
String! readFile(String path) {
  if (!exists(path)) {
    throw FileNotFoundException(path); // This becomes the error value
  }
  return "File Content";
}

B. Propagation (The try Keyword)

In this scenario, we do not want to handle the error locally; we want to pass it up the stack automatically.

String! getConfig() {
  // If readFile fails, the error is immediately returned from getConfig.
  // Execution stops here. The 'try' keyword makes the exit point visible.
  var content = try readFile("config.txt");
  
  return parse(content);
}

C. Handling Errors

To actually handle the error, we use the standard try/catch block, but the inner call remains explicit.

void main() {
  try {
    // We use 'try' here to acknowledge the fallible call.
    // If it fails, it jumps to the catch block as usual.
    var config = try getConfig();
    print("Config loaded: $config");
  } catch (e) {
    // The compiler knows 'getConfig' throws, so this catch is strictly typed.
    print("Failed to load config: $e");
  }
}

4. Backward Compatibility

To ensure non-breaking changes:

  1. Opt-in: Functions without ! remain "Unchecked" (standard Dart behavior).
  2. Gradual Adoption: The analyzer can suggest converting Type to Type! if it detects throwing behavior.
  3. No Syntax Conflicts: try is currently only valid before a { block. Using it as a prefix to an expression (try func()) is syntactically unambiguous to the parser.

Metadata

Metadata

Assignees

No one assigned

    Labels

    featureProposed language feature that solves one or more problems

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions