getodk / getodk/web-forms

Error conditions for external secondary instances

Open
#202 2 comments 0 reactions 0 assignees View on GitHub
documentation needs tech design
Dominant language
TypeScript
Stars
38
Forks
22
PR merge metrics
No merged PRs in 30d

Description

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:

- [I/O support for `jr:` URLs][io-design]
- **[Error conditions][error-conditions]**
- [Engine representation, XPath support][engine-xpath]

[io-design]: https://github.com/getodk/web-forms/issues/201
[error-conditions]: https://github.com/getodk/web-forms/issues/202
[engine-xpath]: https://github.com/getodk/web-forms/issues/203

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](https://en.wikipedia.org/wiki/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][engine-xpath].

## 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`](https://github.com/supermacro/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.

Contributor guide

No contributing guide indexed for this repository

Research direction

No source files or tests are named. Start by reviewing the opening note and the preserved design text to identify the deferred scope, then revise the issue description to reflect the decision and its changed scope; completion is a description consistent with the dedicated error-focused effort.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
backend-api-design
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
15/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.