Skip to content

Error conditions for external secondary instances #202

Description

@eyelidlessness

This was originally opened as part of design for external secondary instances. We ultimately decided to defer the error-focused subset of that design in favor of a dedicated effort for the same. We should revise the text of this issue to reflect that decision and its change in scope! Until then, the previously-scoped text is preserved below...


This design issue is part of broader support for external secondary instances:

This issue is focused on:

  • establishing a goal to define error conditions specific to the external secondary instance feature
  • designing the engine/client interface to document and convey such error conditions
  • anticipating nuances of error conditions as such, and other error conditions not covered by this feature but affected by this design decision

Anticipated error conditions

The error conditions I expect we'll need to handle fall into three buckets:

  • I/O failures (such as poor/intermittent connectivity, verifiable absence of referenced resources)
  • Resources available but malformed
  • Unsupported resource data types

I don't intend to detail all of the specific cases yet, but I think we should be prepared to define a specific set of expected error conditions as a prerequisite for implementation. This way we can plan and design accordingly for the various downstream user flow and/or messaging suitable for each case.

Worth mentioning on that note: depending on how we prioritize functionality, we may want to take special care to distinguish between unsupported data types we know about, and plan to support versus those we simply don't recognize.

Deeper consideration of "error" as a binary state

There's probably room for nuance in both buckets. We might want to also consider some partial and recoverable failure cases:

  • Should unavailability of some subset of form-referenced resources always block form load?
  • Can we detect poor/intermittent connectivity and retry requests?
  • Can we defer failure caused by unavailable resources, until they're required for progress filling/accessing specific subsets of a form?
  • Are there known recovery techniques for certain kinds of malformed data?

I would categorize these as future considerations, out of scope for now unless there's a compelling reason to prioritize otherwise.

But I mention them both because it's good to anticipate future goals, and because they're important to keep in mind as we consider the implications of the next section.

Goal: represent error conditions on par with success conditions

This is something I wish I had made more explicit when I wrote up #188, to avoid confusion on why some of the proposal is shaped the way it is. To be very clear:

  • Documentation-first: an ongoing goal of designs for @getodk/xforms-engine is to make documentation a core work product, in a way that is and remains consistent with the documented functionality.

  • Documentation of failure modes (and other non-happy path cases): a long-discussed goal—now coming into focus as we take on functionality with inherent fallibility—is to ensure that failure modes and error conditions are documented in a way that's consistent with success modes.

There are some fundamental limitations we face here, either imposed by the platform or our development stack (or both). In particular, since we rely heavily on the TypeScript type system to deliver documentation, we must accept that exceptions—and especially the throw keyword—are antithetical to these goals.

Options

Option 0: throw anyway

Maybe we decide that the explicit goals described above are misaligned, or that we have some other way of accommodating those goals besides avoiding throw as an error conveyance mechanism.

If we do this, I would want to have strong assurances that we can provide comparable documentation experience:

  • In editor, for projects/packages depending on @getodk/xforms-engine
  • In our generated API documentation
  • Consistent with actual implementation behavior, over time, verifiable by automation

Option 1: Result type

Fortunately, there's a wealth of prior art we can draw from! A Result type is an obvious candiate to represent the result of explicitly fallible operations. It has very clear practical benefits for our usage:

  • Known error conditions can be documented explicitly, just as successes are
  • Adopting a common representation can help promote patterns that ensure we continue to keep documentation and implementation in sync

One drawback is that a Result type specifically is not necessarily the best representation of an operation which may be partially successful, recoverable, etc. We should be prepared to adapt this approach as appropriate to any of the more ambiguous conditions described above and/or any others we may think of while we prepare for this functionality.

Option 2: some other non-Result value representation

It's possible we decide that a Result type specifically is not up to the task, perhaps because of the nuances of error conditions we expect to handle. This option could be either:

  • Another similar well-known algebraic data type (or composition of more than one)
  • Another custom tagged/discriminated union type of our own design

The latter could have some overlap with other changes we expect to introduce in the engine representation.

What about everything we already throw?

If we decide to go with either option 1 or option 2, there's a nagging problem: we already have quite a lot of throw statements throughout the codebase. Many of which are haphazard, without much if any discernable systematic consideration. Quite a few don't even throw instances of Error! 😰

I don't think this should preclude us from adopting either option. And I don't think it requires us to deal with all of this baggage upfront either. Choosing either option now, with some forethought, can give us a clear framework for improving on this extant problem, and the flexibility to do it as aggressively (or not) as we like.

Especially if we choose option 1, a project I've recently had my eye on would almost certainly be helpful: neverthrow. Aside from providing clear Result-specific patterns with convenient (even approaching idiomatic!) APIs, I also understand it to be pretty pragmatic in how it addresses the awkward "some errors known" situation we'll be in.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    Status
    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions