错误处理

备注

仅当通过设置 > 即将推出的功能 > 预览打开公式级错误管理预览功能时,本文所述的行为才可用。 详细信息:控制启用哪些功能

错误发生。 网络出现故障,存储空间被填满,意想不到的值流入。 您的逻辑在面对潜在问题时能够继续正常工作这一点很重要。

默认情况下,错误流经应用的公式并报告给应用的最终用户。 这样,最终用户知道发生了意外错误,他们可以通过不同的输入自行解决问题,或者他们可以将问题报告给应用的负责人。

作为应用制作者,您可以控制应用中的错误:

  • 检测和处理错误。 如果有可能发生错误,则可以编写应用公式来检测错误情况并重试操作。 最终用户不需要担心发生错误,因为制作者考虑了这种可能性。 这是通过使用公式内的 IfErrorIsErrorIsErrorOrBlank 函数实现的。
  • 报告错误。 如果在遇到错误的公式中未处理错误,则该错误会向上出现到 App.OnError 处理程序中。 在此处,错误不能再被替换,因为它已经发生并且是公式计算的一部分。 但是您可以使用 App.OnError 控制向最终用户报告错误的方式,包括一起隐藏错误报告。 App.OnError 还在整个应用中为错误报告提供了一个通用扼止点。
  • 创建和重新引发错误。 最后,您可以使用自己的逻辑检测错误条件,此条件特定于您的应用。 使用 Error 函数创建自定义错误。 Error 函数还用于在 IfErrorApp.OnError 中查询后重新引发错误。

开始

我们首先来看一个简单的示例。

  1. 在 Power Apps 画布应用中创建一个新屏幕。
  2. 插入 TextInput 控件。 它将默认为名称 TextInput1
  3. 插入一个标签控件。
  4. 将此标签控件的 Text 属性设置为公式
1/Value( TextInput1.Text )

为包含“文本输入”的 text input 控件显示的“无法将值转换为数字”的错误横幅

此时将出现错误,因为 TextInput 控件的默认文本是 "Text input",它不能转换为数字。 默认情况下,这是一件好事:最终用户将收到通知,告知应用中某些功能未按预期运行。

显然,我们不希望用户每次启动此应用时都收到错误提示。 总之,"Text input" 可能不是文本输入框的正确默认值。 为了解决这个问题,我们将 TextInput 控件的 Default 属性更改为:

Blank()

显示“除数为零”的错误横幅

嗯,现在出现了一个不同的错误。 包含空白的数学运算(例如除法)会将空白值强制转换为零。 这现在将导致除以零错误。 为了解决这个问题,我们需要决定在此应用中针对这种情况的适当行为是什么。 答案可能是当文本输入为空白时显示空白。 我们可以通过将公式与 IfError 函数整合来实现这一目标:

IfError( 1/Value( TextInput1.Text ), Blank() )

未显示错误横幅,空白值导致的错误已替换为空白

现在,错误被替换为有效值,并且错误横幅已消失。 但是,我们可能有些过头了,我们使用的 IfError 会处理所有错误,包括输入错误值,例如 "hello"。 我们可以通过调整 IfError 来处理除以零的情况,并重新引发所有其他错误来解决此问题:

IfError( 1/Value( TextInput1.Text ), 
         If( FirstError.Kind = ErrorKind.Div0, Blank(), Error( FirstError ) ) )

未显示错误横幅,明确因除数为零导致的错误已替换为空白,否则将重新抛出错误

因此,我们来运行应用并试用一些不同的值。

如果没有任何值,就像应用启动时那样,则不会显示答案,因为默认值为空白,但也没有显示错误,因为 IfError 将替换除以零错误。

未显示答案,无错误横幅

如果我们键入 4,则得到预期结果 0.25:

显示 0.25,无错误横幅

如果我们输入了一些非法内容,比如 hello,那么我们会收到一个错误横幅:

由于无法将“hello”转换为数字,未显示值和错误横幅

这是一个简单的介绍性示例。 错误处理可以通过多种不同的方式完成,具体取决于应用的需要:

  1. 我们本来可以在带有公式的标签控件中显示 "#Error",而不是错误横幅。 为了使替换的类型与 IfError 的第一个参数兼容,我们需要使用 Text 函数将数值结果显式转换为文本字符串。
    IfError( Text( 1/Value( TextInput1.Text ) ), 
             If( FirstError.Kind = ErrorKind.Div0, Blank(), "#Error" )
    
    没有错误横幅,结果显示 #错误
  2. 我们原本可以编写一个集中式 App.OnError 处理程序,而不是将此特定实例与 IfError 整合。 我们无法替换显示有“#Error”的字符串,因为错误已经发生,并且 App.OnError 仅供控制报告。
    If( FirstError.Kind <> ErrorKind.Div0, Error( FirstError ) )
    

错误传播

错误流经公式的方式与在 Excel 中的流动方式非常相似。 例如,在 Excel 中,如果单元格 A1 的公式为 =1/0,则 A1 将显示错误值 #DIV0!

单元格中显示 A1=1/0 和 #DIV/0! 的 Excel 电子表格

如果单元格 A2 使用诸如 =A1*2 的公式引用 A1,则错误也会通过该公式传播:

单元格中显示 A2=A1*2 和 #DIV/0! 的 Excel 电子表格

该错误会替换本该计算出的值。 单元格中 A2 中的乘法没有结果,只有 A1 中的除法错误。

Power Fx 的工作方式相同。 通常,如果将错误作为函数或运算符的参数提供,则不会发生运算,输入错误将作为运算结果传播。 例如,Mid( Text( 1/0 ), 1, 1 ) 将返回除以零错误,因为会通过 Text 函数和 Mid 函数传递最内层错误:

显示无效操作的错误横幅:除数为零

通常,错误不会通过 Power Apps 控件属性传播。 我们用一个额外的控件来扩展前面的例子,当第一个标签的 Text 属性是错误状态时,会显示此控件:

第二个 label 控件上未显示错误

错误不通过控件传播很好,因为系统会观察所有控件属性的输入错误。 错误不会丢失。

大多数函数和运算符都遵循“错误输入,错误输出”规则,但也有一些例外。 函数 IsErrorIsErrorOrBlankIfError 专为处理错误而设计,因此,即使向它们传递了错误,它们也不会返回错误。

观察错误

在使用错误的值之前,不会观察错误。

因此,即使传入一个错误,IfSelect 函数也可能不会返回错误。 假定有 If( false, 1/0, 3 ) 这个公式。 此公式中存在除以零错误,但由于 If 因遇到 false 而未采用该分支,因此 Power Fx 和 Power Apps 不会报错:

标签 Text 属性中的 If 函数未显示错误横幅

如果使用带错误的 Set 函数,则不会在将错误放入变量时报错。 例如,在 Power Apps 中,App.OnStart 中有一个将除以零错误放入变量 x 中的公式:

App.OnStart 中的 Set 函数调用未显示错误横幅

不会报告任何错误,因为未引用 x。 但是,当我们添加一个标签控件并将其 Text 属性设置为 x 时,会显示错误:

引用变量 x 的 label 控件显示错误横幅

您可能会在具有 IfErrorIsErrorIsErrorOrBlank 函数的公式内观察到错误。 对于这些函数,您可以返回替代值、采取替代操作或在观察和报告错误之前修改错误。

报告错误

观察到错误后,下一步是将错误报告给最终用户。

与 Excel 不同,并不总是有一个方便的地方来显示错误结果,因为公式的结果可能会控制控件的 X 和 Y 坐标等属性,但却没有方便的地方来显示一些文本。 每个 Power Fx 主机控制错误最终如何显示给最终用户,以及制作者对该过程的控制程度。 在 Power Apps 中,将显示错误横幅,App.OnError 用于控制报告错误的方式。

请务必注意,App.OnError 无法像 IfError 那样替换错误。 在执行 App.OnError 时,错误已经发生,已通过其他公式传播结果。 App.OnError 仅控制如何将错误报告给最终用户,并为制作者提供一个挂钩以在需要时记录错误。

范围变量 FirstErrorAllErrors 提供有关一个或多个错误的上下文信息。 这将提供有关错误类型、错误起源位置和观察到错误的位置的信息。

出错后停止

行为公式支持采取行动、修改数据库和更改状态。 这些公式允许使用 ; 链接运算符(或 ;;,具体取决于区域设置)按序列完成多项操作。

例如,在本例中,网格控件将显示 T 表中的内容。 每个按钮选择都通过两个 Patch 调用来更改此表中的状态:

显示表 T 中的两个记录在每次单击按钮后用随机数字更新的动画

在链式行为公式中,操作不会在遇到第一个错误后停止。 我们来修改示例,以在第一个 Patch 调用中传递一个无效的索引号。 尽管出现了之前的错误,但第二个 Patch 仍继续运行。 第一个错误会报告给最终用户,并工作室中的控件上显示为错误:

仅显示表 T 中的第二个记录在每次单击按钮后用随机数更新,第一个记录导致错误的动画

IfError 可用于在出错后停止执行。 与 If 函数类似,此函数的第三个参数提供了一个放置仅在没有错误时才应执行的操作的位置:

显示由于 IfError 阻止错误后的第二个操作完成对表 T 中的记录未进行更改的动画

如果在 ForAll 的其中一个迭代期间遇到错误,则其余迭代不会停止。 ForAll 旨在独立执行每个迭代,允许并行执行。 当 ForAll 完成时,将返回一个错误,其中包含遇到的所有错误(通过检查 IfErrorApp.OnError 中的 AllErrors 来执行)。

例如,以下公式将导致 ForAll 返回两个错误(针对 Value 0 的两次除以零操作),并且 Collection 将有三条记录(Value 不为 0 时各一条):[1, 2, 3]

Clear( Collection ); 
ForAll( [1,0,2,0,3], If( 1/Value > 0, Collect( Collection, Value ) ) );

处理多个错误

由于行为公式可以执行多项操作,因此它也可能会遇到多个错误。

默认情况下,会将第一个错误报告给最终用户。 在本示例中,两次 Patch 调用都将失败,第二次调用具有除以零错误。 仅向用户显示第一个错误(关于索引的错误):

第一个索引错误显示在错误横幅中,第二个错误未报告

IfError 函数和 App.OnError 可以访问使用 AllErrors 范围变量时遇到的所有错误。 在这种情况下,我们可以将其设置为全局变量并查看遇到的这两个错误。 它们按遇到它们的相同顺序出现在表中:

将错误捕获到全局变量 PatchErrors 中,我们可以看到这两个错误都存在

非行为公式也可以返回多个错误。 例如,对要更新的一批记录使用 Patch 函数会返回多个错误,每条失败的记录都有一个错误。

表中的错误

正如我们之前看到的那样,错误可以存储在变量中。 错误也可以包含在数据结构中,例如表中。 这很重要,因此任何一条记录上的错误都不会使整个表无效。

例如,假设 Power Apps 中有以下数据表控件:

显示输入为 0 时“反向”字段的错误的数据表,此错误会导致除数为零错误

对于其中一个值,AddColumns 中的计算遇到了除以零错误。 对于那条记录,Reciprocal 列有一个错误值(除以零),但其他记录没有问题,是好的。 IsError( Index( output, 2 ) ) 返回 false,IsError( Index( output, 2 ).Value ) 返回 true。

如果在筛选表时发生错误,整个记录都是错误的,但仍会在结果中返回,以便最终用户知道产生了一些结果,但结果中存在问题。

举个例子来说。 此处,原始表没有错误,但只要 Value 等于 0,筛选操作就会产生错误:

显示两个无法按筛选条件处理的记录的错误的数据表

值 -5 和 -3 被正确筛选掉。值 0 会导致处理筛选器时出错,因此不清楚该记录是否应包含在结果中。 为了最大限度地提高对最终用户的透明度并帮助制作者进行调试,我们包含了一条错误记录来代替原始记录。 在本例中,IsError( Index( output, 2 ) ) 返回 true。

数据源错误

数据源中 PatchCollectRemoveRemoveIfUpdateUpdateIfSubmitForm 等修改数据的函数以两种方式报错:

  • 这些函数中的每一个都将返回一个错误值作为操作的结果。 可以像往常一样使用 IsError 检测错误,并使用 IfErrorApp.OnError 替换或隐藏错误。
  • 在操作之后,Errors 函数还将返回以前操作的错误。 这对于在窗体屏幕上显示错误消息而不需要在状态变量中捕获错误很有用。

例如,此公式将检查 Collect 中的错误并显示自定义错误消息:

IfError( Collect( Names, { Name: "duplicate" } ),
         Notify( $"OOPS: { FirstError.Message }", NotificationType.Warning ) )

Errors 函数还返回有关运行时操作期间的过去错误的信息。 它对于在窗体屏幕上显示错误而不需要在状态变量中捕获错误很有用。

重新引发错误

有时,一些潜在的错误是预料之中的,可以放心地忽略。 在 IfErrorApp.OnError 内,如果检测到应传递到下一个更高级处理程序的错误,则可以使用 Error( AllErrors ) 重新引发该错误。

创建自己的错误

您还可以使用 Error 函数创建自己的错误。

如果您要创建自己的错误,建议您使用 1000 以上的值,以避免与未来的系统错误值发生潜在冲突。

ErrorKind 枚举值

ErrorKind 枚举 价值 说明
AnalysisError 18 系统错误。 编译器分析有问题。
BadLanguageCode 14 使用了无效或无法识别的语言代码。
BadRegex 15 正则表达式无效。 检查与 IsMatchMatchMatchAll 函数配合使用的语法。
冲突 6 正在更新的记录已在源处更改,需要解决冲突。 常见的解决方案是保存任何本地更改、刷新记录并重新应用更改。
ConstraintViolated 8 该记录未通过服务器上的约束检查。
CreatePermission 3 用户没有适用于数据源的“创建记录”权限。 例如,调用了 Collect 函数。
DeletePermissions 5 用户没有适用于数据源的“删除记录”权限。 例如,调用了 Remove 函数。
Div0 13 除数为零。
EditPermissions 4 用户没有适用于数据源的“创建记录”权限。 例如,调用了 Patch 函数。
GeneratedValue 9 对于由服务器自动计算的字段,值被错误地传递到服务器。
InvalidFunctionUsage 16 函数使用无效。 函数的一个或多个参数通常不正确或以其使用方式无效。
FileNotFound 17 找不到 SaveData 存储区域。
InsufficientMemory 21 设备中没有足够的内存或存储空间来执行操作。
InvalidArgument 25 向函数传递了无效参数。
内部 26 系统错误。 其中一个函数存在内部问题。
MissingRequired 2 缺少记录的必填字段。
网络 23 网络通信出现问题。
None 12 系统错误。 不存在错误。
不适用 27 无可用值。 可用于区分在数值计算中可能被视为零的空白值与使用时应标记为潜在问题的空白值。
NotFound 7 找不到记录。 例如,要在 Patch 函数中修改的记录。
NotSupported 20 此播放器或设备不支持的操作。
数字 24 数字函数的使用方式错误。 例如,Sqrt 中有 -1。
QuoteExceeded 22 已超出存储配额。
ReadOnlyValue 10 列是只读列,无法被修改。
ReadPermission 19 用户没有适用于数据源的“读取记录”权限。
同步 1 数据源报告了错误。 查看“消息”列了解详细信息。
未知 12 存在未知类型的错误。
验证 11 记录未通过验证检查。