Skip to content

Commit 8b30a1e

Browse files
authored
refactor: streamline error handling (#1141)
## Description I decided that defining a specific error for not meeting the contract is probably an overkill, as the input layer can be ine, output layer can be wrong and we already have the code that can distinguish between those. I added comments and I refactored the both native and JS error handling a bit It also fixes a few typos & bugs ### Introduces a breaking change? - [ ] Yes - [ ] No ### Type of change - [ ] Bug fix (change which fixes an issue) - [ ] New feature (change which adds functionality) - [ ] Documentation update (improves or adds clarity to existing documentation) - [x] Other (chores, tests, code style improvements etc.) ### Tested on - [x] iOS - [x] Android ### Testing instructions - Trigger a download interruption (kill network mid-fetch, or pass a bad URL) for any model — confirm thrown error has code DownloadInterrupted and the expected "The download has been interrupted..." message - Call forward() on a module before load() resolves — confirm code ModuleNotLoaded and "...before calling forward()." message - Kick off a second forward() while the first is still running — confirm code ModelGenerating and "...wait until previous model run is complete." message - Hit one of the overridden sites to make sure custom messages still flow through: e.g. LLMController.generate() with empty messages array (should say "Messages array is empty!", code InvalidUserInput), or BaseOCRController.internalLoad with an unsupported language ("...not supported. Please try using other language.", code LanguageNotSupported) - Spot-check a couple modules across categories — LLM, OCR, SpeechToText, VAD, TextToImage, Classification — make sure load + forward still work end-to-end on device (the constructor signature change shouldn't affect runtime) - Verify error.code is still set correctly on caught errors in JS (try/catch then check e.code and e.message) ### Screenshots <!-- Add screenshots here, if applicable --> ### Related issues <!-- Link related issues here using #issue-number --> ### Checklist - [ ] I have performed a self-review of my code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have updated the documentation accordingly - [ ] My changes generate no new warnings ### Additional notes <!-- Include any additional information, assumptions, or context that reviewers might need to understand this PR. -->
1 parent 49d3b5a commit 8b30a1e

44 files changed

Lines changed: 204 additions & 316 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/docs/05-utilities/04-error-handling.md

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,9 @@ llm.delete();
6868

6969
## Reference
7070

71-
All errors in React Native ExecuTorch inherit from `RnExecutorchError` and include a `code` property from the `RnExecutorchErrorCode` enum. Below is a comprehensive list of all possible errors, organized by category.
71+
All errors in React Native ExecuTorch inherit from `RnExecutorchError` and include a `code` property. The code is typed as `RnExecutorchErrorCode | number`: in practice you'll almost always see a member of the enum, but the codes are generated from a shared config into both the C++ and TS sources, and if those generated files drift the raw numeric value flows through unchanged. When switching on `err.code`, always include a `default` branch.
72+
73+
Below is a comprehensive list of all possible errors, organized by category.
7274

7375
### Module State Errors
7476

@@ -83,15 +85,15 @@ These errors occur when trying to perform operations on a model in an invalid st
8385

8486
These errors occur when invalid configuration or input is provided.
8587

86-
| Error Code | Description | When It Occurs | How to Handle |
87-
| ---------------------- | ------------------------------------ | --------------------------------------------------------------------------------------- | -------------------------------------------------------- |
88-
| `InvalidConfig` | Configuration parameters are invalid | Setting parameters outside valid ranges (e.g., `topp` outside [0, 1]) | Check parameter constraints and provide valid values |
89-
| `InvalidUserInput` | Input provided to API is invalid | Passing empty arrays, null values, or malformed data to methods | Validate input before calling methods |
90-
| `InvalidModelSource` | Model source type is invalid | Providing wrong type for model source (e.g., object when string expected) | Ensure model source matches expected type |
91-
| `LanguageNotSupported` | Language not supported by model | Passing unsupported language to multilingual OCR or Speech-to-Text models | Use a supported language or different model |
92-
| `PlatformNotSupported` | Current platform is not supported | Using features (e.g., camera frame processing) on an unsupported platform or OS version | Ensure you're running on a supported platform/OS version |
93-
| `WrongDimensions` | Input tensor dimensions don't match | Providing input with incorrect shape for the model | Check model's expected input dimensions |
94-
| `UnexpectedNumInputs` | Wrong number of inputs provided | Passing more or fewer inputs than model expects | Match the number of inputs to model metadata |
88+
| Error Code | Description | When It Occurs | How to Handle |
89+
| ---------------------- | ------------------------------------ | --------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
90+
| `InvalidConfig` | Configuration parameters are invalid | Setting parameters outside valid ranges (e.g., `topp` outside [0, 1]) | Check parameter constraints and provide valid values |
91+
| `InvalidUserInput` | Input provided to API is invalid | Passing empty arrays, null values, or malformed data to methods | Validate input before calling methods |
92+
| `InvalidModelSource` | Model source type is invalid | Providing wrong type for model source (e.g., object when string expected) | Ensure model source matches expected type |
93+
| `LanguageNotSupported` | Language not supported by model | Passing unsupported language to multilingual OCR or Speech-to-Text models | Use a supported language or different model |
94+
| `PlatformNotSupported` | Current platform is not supported | Using features (e.g., camera frame processing) on an unsupported platform or OS version | Ensure you're running on a supported platform/OS version |
95+
| `WrongDimensions` | Input tensor dimensions don't match | Providing input with incorrect shape for the model | Check model's expected input dimensions |
96+
| `UnexpectedNumInputs` | Wrong number of inputs provided | Passing more or fewer inputs than model expects | Verify the expected model I/O contract in docs or source code |
9597

9698
### File Operations Errors
9799

@@ -133,12 +135,12 @@ These errors are specific to streaming transcription operations.
133135

134136
These errors come from the ExecuTorch runtime during model execution.
135137

136-
| Error Code | Description | When It Occurs | How to Handle |
137-
| -------------------- | -------------------------------- | ---------------------------------------------- | ---------------------------------------------- |
138-
| `InvalidModelOutput` | Model output size unexpected | Model produces output of wrong size | Verify model is compatible with the library |
139-
| `ThreadPoolError` | Threadpool operation failed | Internal threading issue | Restart the model or app |
140-
| `TokenizerError` | Tokenizer or tokenization failed | Tokenizer initialization or processing error | Check tokenizer files and model compatibility |
141-
| `UnknownError` | Unexpected error occurred | 3rd-party library error or unhandled exception | Check logs for details, report if reproducible |
138+
| Error Code | Description | When It Occurs | How to Handle |
139+
| -------------------- | -------------------------------- | ---------------------------------------------- | ----------------------------------------------------------- |
140+
| `InvalidModelOutput` | Model output size unexpected | Model produces output of wrong size | Verify model's expected I/O contract in docs or source code |
141+
| `ThreadPoolError` | Threadpool operation failed | Internal threading issue | Restart the model or app |
142+
| `TokenizerError` | Tokenizer or tokenization failed | Tokenizer initialization or processing error | Check tokenizer files and model compatibility |
143+
| `UnknownError` | Unexpected error occurred | 3rd-party library error or unhandled exception | Check logs for details, report if reproducible |
142144

143145
### ExecuTorch Runtime Errors
144146

packages/react-native-executorch/common/rnexecutorch/Error.h

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
#pragma once
22

33
#include <executorch/runtime/core/error.h>
4+
#include <source_location>
45
#include <stdexcept>
56
#include <string>
7+
#include <string_view>
68
#include <variant>
79

810
#include <rnexecutorch/ErrorCodes.h>
@@ -34,4 +36,40 @@ class RnExecutorchError : public std::runtime_error {
3436
}
3537
};
3638

39+
namespace detail {
40+
41+
// npos + 1 wraps to 0, so the no-slash case returns the whole path.
42+
constexpr std::string_view basename(std::string_view path) noexcept {
43+
return path.substr(path.find_last_of('/') + 1);
44+
}
45+
46+
inline std::string locationPrefix(const std::source_location &loc) {
47+
return "[" + std::string(basename(loc.file_name())) + "] ";
48+
}
49+
50+
[[noreturn]] inline void
51+
throwNotLoaded(std::source_location loc = std::source_location::current()) {
52+
throw RnExecutorchError(RnExecutorchErrorCode::ModuleNotLoaded,
53+
locationPrefix(loc) + "Model not loaded (in: " +
54+
loc.function_name() + ")");
55+
}
56+
57+
template <typename Result>
58+
inline void checkOkOrThrowForwardError(
59+
const Result &result,
60+
std::source_location loc = std::source_location::current()) {
61+
if (!result.ok()) {
62+
throw RnExecutorchError(
63+
result.error(), locationPrefix(loc) +
64+
"Forward pass failed (in: " + loc.function_name() +
65+
"). Ensure the model input is correct.");
66+
}
67+
}
68+
69+
} // namespace detail
3770
} // namespace rnexecutorch
71+
72+
#define CHECK_OK_OR_THROW_FORWARD_ERROR(result) \
73+
::rnexecutorch::detail::checkOkOrThrowForwardError(result)
74+
75+
#define THROW_NOT_LOADED_ERROR() ::rnexecutorch::detail::throwNotLoaded()

packages/react-native-executorch/common/rnexecutorch/ErrorCodes.h

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ enum class RnExecutorchErrorCode : int32_t {
4848
*/
4949
FileReadFailed = 114,
5050
/**
51-
* Thrown when the size of model output is unexpected.
51+
* Thrown when the size of model output is unexpected. If you're using your
52+
* custom model with any of the pre-defined modules, please verify docs or
53+
* source code for the expected model I/O contract.
5254
*/
5355
InvalidModelOutput = 115,
5456
/**
@@ -77,7 +79,9 @@ enum class RnExecutorchErrorCode : int32_t {
7779
InvalidModelSource = 120,
7880
/**
7981
* Thrown when the number of passed inputs to the model is different than the
80-
* model metadata specifies.
82+
* model metadata specifies. If you're using your custom model with any of the
83+
* pre-defined modules, please verify docs or source code for the expected
84+
* model I/O contract.
8185
*/
8286
UnexpectedNumInputs = 121,
8387
/**

packages/react-native-executorch/common/rnexecutorch/TokenizerModule.cpp

Lines changed: 27 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -18,24 +18,18 @@ TokenizerModule::TokenizerModule(
1818
auto status = tokenizer->load(source);
1919

2020
if (status != tokenizers::Error::Ok) {
21-
throw RnExecutorchError(RnExecutorchErrorCode::TokenizerError,
22-
"Unexpected issue occured while loading tokenizer");
21+
throw RnExecutorchError(
22+
RnExecutorchErrorCode::TokenizerError,
23+
"Unexpected issue occurred while loading tokenizer");
2324
};
2425
std::filesystem::path modelPath{source};
2526
memorySizeLowerBound = std::filesystem::file_size(modelPath);
2627
}
2728

28-
void TokenizerModule::ensureTokenizerLoaded(
29-
const std::string &methodName) const {
29+
std::vector<uint64_t> TokenizerModule::encode(std::string s) const {
3030
if (!tokenizer) {
31-
throw RnExecutorchError(
32-
RnExecutorchErrorCode::ModuleNotLoaded,
33-
methodName + " function was called on an uninitialized tokenizer!");
31+
THROW_NOT_LOADED_ERROR();
3432
}
35-
}
36-
37-
std::vector<uint64_t> TokenizerModule::encode(std::string s) const {
38-
ensureTokenizerLoaded("encode");
3933

4034
// If the used tokenizer.json has defined post_processor field,
4135
// setting any of bos or eos arguments to value other than provided constant
@@ -44,54 +38,62 @@ std::vector<uint64_t> TokenizerModule::encode(std::string s) const {
4438
auto encodeResult =
4539
tokenizer->encode(s, numOfAddedBoSTokens, numOfAddedEoSTokens);
4640
if (!encodeResult.ok()) {
47-
throw rnexecutorch::RnExecutorchError(
48-
rnexecutorch::RnExecutorchErrorCode::TokenizerError,
49-
"Unexpected issue occured while encoding: " +
41+
throw RnExecutorchError(
42+
RnExecutorchErrorCode::TokenizerError,
43+
"Unexpected issue occurred while encoding: " +
5044
std::to_string(static_cast<int32_t>(encodeResult.error())));
5145
}
5246
return encodeResult.get();
5347
}
5448

5549
std::string TokenizerModule::decode(std::vector<uint64_t> vec,
5650
bool skipSpecialTokens) const {
57-
ensureTokenizerLoaded("decode");
51+
if (!tokenizer) {
52+
THROW_NOT_LOADED_ERROR();
53+
}
5854

5955
auto decodeResult = tokenizer->decode(vec, skipSpecialTokens);
6056
if (!decodeResult.ok()) {
6157
throw RnExecutorchError(
6258
RnExecutorchErrorCode::TokenizerError,
63-
"Unexpected issue occured while decoding: " +
59+
"Unexpected issue occurred while decoding: " +
6460
std::to_string(static_cast<int32_t>(decodeResult.error())));
6561
}
6662

6763
return decodeResult.get();
6864
}
6965

7066
size_t TokenizerModule::getVocabSize() const {
71-
ensureTokenizerLoaded("getVocabSize");
67+
if (!tokenizer) {
68+
THROW_NOT_LOADED_ERROR();
69+
}
7270
return static_cast<size_t>(tokenizer->vocab_size());
7371
}
7472

7573
std::string TokenizerModule::idToToken(uint64_t tokenId) const {
76-
ensureTokenizerLoaded("idToToken");
74+
if (!tokenizer) {
75+
THROW_NOT_LOADED_ERROR();
76+
}
7777
auto result = tokenizer->id_to_piece(tokenId);
7878
if (!result.ok()) {
79-
throw rnexecutorch::RnExecutorchError(
80-
rnexecutorch::RnExecutorchErrorCode::TokenizerError,
81-
"Unexpected issue occured while trying to convert id to token: " +
79+
throw RnExecutorchError(
80+
RnExecutorchErrorCode::TokenizerError,
81+
"Unexpected issue occurred while converting id to token: " +
8282
std::to_string(static_cast<int32_t>(result.error())));
8383
}
8484
return result.get();
8585
}
8686

8787
uint64_t TokenizerModule::tokenToId(std::string token) const {
88-
ensureTokenizerLoaded("tokenToId");
88+
if (!tokenizer) {
89+
THROW_NOT_LOADED_ERROR();
90+
}
8991

9092
auto result = tokenizer->piece_to_id(token);
9193
if (!result.ok()) {
92-
throw rnexecutorch::RnExecutorchError(
93-
rnexecutorch::RnExecutorchErrorCode::TokenizerError,
94-
"Unexpected issue occured while trying to convert token to id: " +
94+
throw RnExecutorchError(
95+
RnExecutorchErrorCode::TokenizerError,
96+
"Unexpected issue occurred while converting token to id: " +
9597
std::to_string(static_cast<int32_t>(result.error())));
9698
}
9799
return result.get();

packages/react-native-executorch/common/rnexecutorch/TokenizerModule.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ class TokenizerModule {
2424
std::size_t getMemoryLowerBound() const noexcept;
2525

2626
private:
27-
void ensureTokenizerLoaded(const std::string &methodName) const;
2827
std::unique_ptr<tokenizers::HFTokenizer> tokenizer;
2928
std::size_t memorySizeLowerBound{0};
3029
};

packages/react-native-executorch/common/rnexecutorch/models/BaseModel.cpp

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,7 @@ BaseModel::BaseModel(const std::string &modelSource,
3131
std::vector<int32_t> BaseModel::getInputShape(std::string method_name,
3232
int32_t index) const {
3333
if (!module_) {
34-
throw RnExecutorchError(RnExecutorchErrorCode::ModuleNotLoaded,
35-
"Model not loaded: Cannot get input shape");
34+
THROW_NOT_LOADED_ERROR();
3635
}
3736

3837
auto method_meta = module_->method_meta(method_name);
@@ -58,8 +57,7 @@ std::vector<int32_t> BaseModel::getInputShape(std::string method_name,
5857
std::vector<std::vector<int32_t>>
5958
BaseModel::getAllInputShapes(std::string methodName) const {
6059
if (!module_) {
61-
throw RnExecutorchError(RnExecutorchErrorCode::ModuleNotLoaded,
62-
"Model not loaded: Cannot get all input shapes");
60+
THROW_NOT_LOADED_ERROR();
6361
}
6462

6563
auto method_meta = module_->method_meta(methodName);
@@ -91,8 +89,7 @@ BaseModel::getAllInputShapes(std::string methodName) const {
9189
std::vector<JSTensorViewOut>
9290
BaseModel::forwardJS(std::vector<JSTensorViewIn> tensorViewVec) const {
9391
if (!module_) {
94-
throw RnExecutorchError(RnExecutorchErrorCode::ModuleNotLoaded,
95-
"Model not loaded: Cannot perform forward pass");
92+
THROW_NOT_LOADED_ERROR();
9693
}
9794
std::vector<executorch::runtime::EValue> evalues;
9895
evalues.reserve(tensorViewVec.size());
@@ -114,11 +111,7 @@ BaseModel::forwardJS(std::vector<JSTensorViewIn> tensorViewVec) const {
114111
}
115112

116113
auto result = module_->forward(evalues);
117-
if (!result.ok()) {
118-
throw RnExecutorchError(result.error(),
119-
"The model's forward function did not succeed. "
120-
"Ensure the model input is correct.");
121-
}
114+
CHECK_OK_OR_THROW_FORWARD_ERROR(result);
122115

123116
auto &outputs = result.get();
124117
std::vector<JSTensorViewOut> output;
@@ -141,26 +134,23 @@ BaseModel::forwardJS(std::vector<JSTensorViewIn> tensorViewVec) const {
141134
Result<executorch::runtime::MethodMeta>
142135
BaseModel::getMethodMeta(const std::string &methodName) const {
143136
if (!module_) {
144-
throw RnExecutorchError(RnExecutorchErrorCode::ModuleNotLoaded,
145-
"Model not loaded: Cannot get method meta");
137+
THROW_NOT_LOADED_ERROR();
146138
}
147139
return module_->method_meta(methodName);
148140
}
149141

150142
Result<std::vector<EValue>>
151143
BaseModel::forward(const EValue &input_evalue) const {
152144
if (!module_) {
153-
throw RnExecutorchError(RnExecutorchErrorCode::ModuleNotLoaded,
154-
"Model not loaded: Cannot perform forward pass");
145+
THROW_NOT_LOADED_ERROR();
155146
}
156147
return module_->forward(input_evalue);
157148
}
158149

159150
Result<std::vector<EValue>>
160151
BaseModel::forward(const std::vector<EValue> &input_evalues) const {
161152
if (!module_) {
162-
throw RnExecutorchError(RnExecutorchErrorCode::ModuleNotLoaded,
163-
"Model not loaded: Cannot perform forward pass");
153+
THROW_NOT_LOADED_ERROR();
164154
}
165155
return module_->forward(input_evalues);
166156
}
@@ -169,8 +159,7 @@ Result<std::vector<EValue>>
169159
BaseModel::execute(const std::string &methodName,
170160
const std::vector<EValue> &input_value) const {
171161
if (!module_) {
172-
throw RnExecutorchError(RnExecutorchErrorCode::ModuleNotLoaded,
173-
"Model not loaded, cannot run execute");
162+
THROW_NOT_LOADED_ERROR();
174163
}
175164
return module_->execute(methodName, input_value);
176165
}

packages/react-native-executorch/common/rnexecutorch/models/classification/Classification.cpp

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,7 @@ Classification::runInference(cv::Mat image) {
6060
preprocessed);
6161

6262
auto forwardResult = BaseModel::forward(inputTensor);
63-
if (!forwardResult.ok()) {
64-
throw RnExecutorchError(forwardResult.error(),
65-
"The model's forward function did not succeed. "
66-
"Ensure the model input is correct.");
67-
}
63+
CHECK_OK_OR_THROW_FORWARD_ERROR(forwardResult);
6864
return postprocess(forwardResult->at(0).toTensor());
6965
}
7066

packages/react-native-executorch/common/rnexecutorch/models/embeddings/image/ImageEmbeddings.cpp

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,7 @@ ImageEmbeddings::runInference(cv::Mat image) {
3838

3939
auto forwardResult = BaseModel::forward(inputTensor);
4040

41-
if (!forwardResult.ok()) {
42-
throw RnExecutorchError(
43-
forwardResult.error(),
44-
"The model's forward function did not succeed. Ensure the model input "
45-
"is correct.");
46-
}
41+
CHECK_OK_OR_THROW_FORWARD_ERROR(forwardResult);
4742

4843
auto forwardResultTensor = forwardResult->at(0).toTensor();
4944
return std::make_shared<OwningArrayBuffer>(

packages/react-native-executorch/common/rnexecutorch/models/embeddings/text/TextEmbeddings.cpp

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,7 @@ TextEmbeddings::generate(const std::string input) {
5656
attnMaskShape, preprocessed.attentionMask.data(), ScalarType::Long);
5757

5858
auto forwardResult = BaseModel::forward({tokenIds, attnMask});
59-
if (!forwardResult.ok()) {
60-
throw RnExecutorchError(
61-
forwardResult.error(),
62-
"The model's forward function did not succeed. Ensure the model input "
63-
"is correct.");
64-
}
59+
CHECK_OK_OR_THROW_FORWARD_ERROR(forwardResult);
6560

6661
return BaseEmbeddings::postprocess(forwardResult);
6762
}

packages/react-native-executorch/common/rnexecutorch/models/instance_segmentation/BaseInstanceSegmentation.cpp

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -79,13 +79,7 @@ std::vector<types::Instance> BaseInstanceSegmentation::runInference(
7979

8080
auto forwardResult =
8181
BaseModel::execute(methodName, {buildInputTensor(image)});
82-
if (!forwardResult.ok()) {
83-
throw RnExecutorchError(
84-
forwardResult.error(),
85-
"The model's forward function did not succeed. "
86-
"Ensure the model input is correct and method name '" +
87-
methodName + "' is valid.");
88-
}
82+
CHECK_OK_OR_THROW_FORWARD_ERROR(forwardResult);
8983

9084
validateOutputTensors(forwardResult.get());
9185

0 commit comments

Comments
 (0)